From f7db279447fb2f357f250f5804b3fda108207dc2 Mon Sep 17 00:00:00 2001 From: ioedeveloper Date: Tue, 14 Sep 2021 03:52:13 +0100 Subject: [PATCH] publish to gist --- .../file-explorer/src/lib/file-explorer.tsx | 153 +--------------- .../file-explorer/src/lib/types/index.ts | 2 - .../workspace/src/lib/actions/workspace.ts | 165 +++++++++++++++++- .../workspace/src/lib/contexts/index.ts | 3 +- .../src/lib/providers/FileSystemProvider.tsx | 16 +- .../workspace/src/lib/reducers/workspace.ts | 22 ++- .../workspace/src/lib/remix-ui-workspace.tsx | 4 - 7 files changed, 200 insertions(+), 165 deletions(-) diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx index 6fac3cdcff..789bdffa1f 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx @@ -2,22 +2,18 @@ import React, { useEffect, useState, useRef, useReducer, useContext } from 'reac // import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line import { Toaster } from '@remix-ui/toaster' // eslint-disable-line -import Gists from 'gists' import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line import { FileExplorerProps, File, MenuItems, FileExplorerState } from './types' import * as helper from '../../../../../apps/remix-ide/src/lib/helper' -import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' import { FileSystemContext } from '@remix-ui/workspace' import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' import { contextMenuActions } from './utils' import './css/file-explorer.css' -const queryParams = new QueryParams() - export const FileExplorer = (props: FileExplorerProps) => { - const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems, resetFocus, files } = props + const { name, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems, resetFocus, files } = props const [state, setState] = useState({ focusElement: [{ key: '', @@ -72,16 +68,6 @@ export const FileExplorer = (props: FileExplorerProps) => { } }, [state.focusEdit.element]) - useEffect(() => { - (async () => { - const fileManager = registry.get('filemanager').api - - setState(prevState => { - return { ...prevState, fileManager, expandPath: [name] } - }) - })() - }, [name]) - useEffect(() => { if (focusRoot) { setState(prevState => { @@ -375,103 +361,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } const toGist = (path?: string, type?: string) => { - const filesProvider = fileSystem.provider.provider - const proccedResult = function (error, data) { - if (error) { - global.modal('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', () => {}) - } else { - if (data.html_url) { - global.modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', () => { - window.open(data.html_url, '_blank') - }, 'Cancel', () => {}) - } else { - const error = JSON.stringify(data.errors, null, '\t') || '' - const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message - global.modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', () => {}) - } - } - } - - /** - * This function is to get the original content of given gist - * @params id is the gist id to fetch - */ - const getOriginalFiles = async (id) => { - if (!id) { - return [] - } - - const url = `https://api.github.com/gists/${id}` - const res = await fetch(url) - const data = await res.json() - return data.files || [] - } - - // If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer. - const folder = path || '/' - const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null - - packageFiles(filesProvider, folder, async (error, packaged) => { - if (error) { - console.log(error) - global.modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', async () => {}) - } else { - // check for token - const config = registry.get('config').api - const accessToken = config.get('settings/gist-access-token') - - if (!accessToken) { - global.modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', () => {}) - } else { - const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' + - queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist=' - const gists = new Gists({ token: accessToken }) - - if (id) { - const originalFileList = await getOriginalFiles(id) - // Telling the GIST API to remove files - const updatedFileList = Object.keys(packaged) - const allItems = Object.keys(originalFileList) - .filter(fileName => updatedFileList.indexOf(fileName) === -1) - .reduce((acc, deleteFileName) => ({ - ...acc, - [deleteFileName]: null - }), originalFileList) - // adding new files - updatedFileList.forEach((file) => { - const _items = file.split('/') - const _fileName = _items[_items.length - 1] - allItems[_fileName] = packaged[file] - }) - - toast('Saving gist (' + id + ') ...') - gists.edit({ - description: description, - public: true, - files: allItems, - id: id - }, (error, result) => { - proccedResult(error, result) - if (!error) { - for (const key in allItems) { - if (allItems[key] === null) delete allItems[key] - } - } - }) - } else { - // id is not existing, need to create a new gist - toast('Creating a new gist ...') - gists.create({ - description: description, - public: true, - files: packaged - }, (error, result) => { - proccedResult(error, result) - }) - } - } - } - }) + global.dispatchPublishToGist(path, type) } const runScript = async (path: string) => { @@ -878,45 +768,6 @@ export const FileExplorer = (props: FileExplorerProps) => { export default FileExplorer -async function packageFiles (filesProvider, directory, callback) { - const isFile = filesProvider.isFile(directory) - const ret = {} - - if (isFile) { - try { - filesProvider.get(directory, (error, content) => { - if (error) throw new Error('An error ocurred while getting file content. ' + directory) - if (/^\s+$/.test(content) || !content.length) { - content = '// this line is added to create a gist. Empty file is not allowed.' - } - directory = directory.replace(/\//g, '...') - ret[directory] = { content } - callback(null, ret) - }) - } catch (e) { - return callback(e) - } - } else { - try { - await filesProvider.copyFolderToJson(directory, ({ path, content }) => { - if (/^\s+$/.test(content) || !content.length) { - content = '// this line is added to create a gist. Empty file is not allowed.' - } - if (path.indexOf('gist-') === 0) { - path = path.split('/') - path.shift() - path = path.join('/') - } - path = path.replace(/\//g, '...') - ret[path] = { content } - }) - callback(null, ret) - } catch (e) { - return callback(e) - } - } -} - function joinPath (...paths) { paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) if (paths.length === 1) return paths[0] diff --git a/libs/remix-ui/file-explorer/src/lib/types/index.ts b/libs/remix-ui/file-explorer/src/lib/types/index.ts index c1f8221688..203674d16e 100644 --- a/libs/remix-ui/file-explorer/src/lib/types/index.ts +++ b/libs/remix-ui/file-explorer/src/lib/types/index.ts @@ -4,8 +4,6 @@ export type MenuItems = action[] // eslint-disable-line no-use-before-define /* eslint-disable-next-line */ export interface FileExplorerProps { name: string, - registry: any, - filesProvider: any, menuItems?: string[], plugin: any, focusRoot: boolean, diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index 09e43aad38..06095287fd 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -1,7 +1,8 @@ import React from 'react' import { bufferToHex, keccakFromString } from 'ethereumjs-util' import axios, { AxiosResponse } from 'axios' -import { checkSpecialChars, checkSlash, extractParentFromKey } from '@remix-ui/helper' +import { checkSpecialChars, checkSlash, extractParentFromKey, extractNameFromKey } from '@remix-ui/helper' +import Gists from 'gists' const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples') @@ -9,6 +10,7 @@ const queuedEvents = [] const pendingEvents = {} const LOCALHOST = ' - connect to localhost - ' const NO_WORKSPACE = ' - none - ' +const queryParams = new QueryParams() let plugin, dispatch: React.Dispatch @@ -179,6 +181,19 @@ const setDeleteWorkspace = (workspaceName: string) => { } } +const displayPopUp = (message: string) => { + return { + type: 'DISPLAY_POPUP_MESSAGE', + payload: message + } +} + +const hidePopUp = () => { + return { + type: 'HIDE_POPUP_MESSAGE' + } +} + const createWorkspaceTemplate = async (workspaceName: string, setDefaults = true, template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => { if (!workspaceName) throw new Error('workspace name cannot be empty') if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') @@ -188,7 +203,6 @@ const createWorkspaceTemplate = async (workspaceName: string, setDefaults = true await workspaceProvider.createWorkspace(workspaceName) if (setDefaults) { - const queryParams = new QueryParams() const params = queryParams.get() switch (template) { @@ -316,6 +330,82 @@ const getWorkspaces = async (): Promise | undefined => { } } +const packageGistFiles = async (directory) => { + return new Promise((resolve, reject) => { + const workspaceProvider = plugin.fileProviders.workspace + const isFile = workspaceProvider.isFile(directory) + const ret = {} + + if (isFile) { + try { + workspaceProvider.get(directory, (error, content) => { + if (error) throw new Error('An error ocurred while getting file content. ' + directory) + if (/^\s+$/.test(content) || !content.length) { + content = '// this line is added to create a gist. Empty file is not allowed.' + } + directory = directory.replace(/\//g, '...') + ret[directory] = { content } + return resolve(ret) + }) + } catch (e) { + return reject(e) + } + } else { + try { + (async () => { + await workspaceProvider.copyFolderToJson(directory, ({ path, content }) => { + if (/^\s+$/.test(content) || !content.length) { + content = '// this line is added to create a gist. Empty file is not allowed.' + } + if (path.indexOf('gist-') === 0) { + path = path.split('/') + path.shift() + path = path.join('/') + } + path = path.replace(/\//g, '...') + ret[path] = { content } + }) + resolve(ret) + })() + } catch (e) { + return reject(e) + } + } + }) +} + +const handleGistResponse = (error, data) => { + if (error) { + dispatch(displayNotification('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', null)) + } else { + if (data.html_url) { + dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => { + window.open(data.html_url, '_blank') + }, () => {})) + } else { + const error = JSON.stringify(data.errors, null, '\t') || '' + const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message + + dispatch(displayNotification('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', null)) + } + } +} + +/** + * This function is to get the original content of given gist + * @params id is the gist id to fetch + */ +const getOriginalFiles = async (id) => { + if (!id) { + return [] + } + + const url = `https://api.github.com/gists/${id}` + const res = await fetch(url) + const data = await res.json() + return data.files || [] +} + const listenOnEvents = (provider) => { provider.event.on('fileAdded', async (filePath: string) => { await executeEvent('fileAdded', filePath) @@ -390,7 +480,6 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React. dispatch = reducerDispatch const workspaceProvider = filePanelPlugin.fileProviders.workspace const localhostProvider = filePanelPlugin.fileProviders.localhost - const queryParams = new QueryParams() const params = queryParams.get() const workspaces = await getWorkspaces() || [] @@ -546,6 +635,76 @@ export const deleteWorkspace = (workspaceName: string) => async (dispatch: React await dispatch(setDeleteWorkspace(workspaceName)) } +export const publishToGist = (path?: string, type?: string) => async (dispatch: React.Dispatch) => { + // If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer. + const folder = path || '/' + const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null + try { + const packaged = await packageGistFiles(folder) + // check for token + const config = plugin.registry.get('config').api + const accessToken = config.get('settings/gist-access-token') + + if (!accessToken) { + dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => {})) + } else { + const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' + + queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist=' + const gists = new Gists({ token: accessToken }) + + if (id) { + const originalFileList = await getOriginalFiles(id) + // Telling the GIST API to remove files + const updatedFileList = Object.keys(packaged) + const allItems = Object.keys(originalFileList) + .filter(fileName => updatedFileList.indexOf(fileName) === -1) + .reduce((acc, deleteFileName) => ({ + ...acc, + [deleteFileName]: null + }), originalFileList) + // adding new files + updatedFileList.forEach((file) => { + const _items = file.split('/') + const _fileName = _items[_items.length - 1] + allItems[_fileName] = packaged[file] + }) + + dispatch(displayPopUp('Saving gist (' + id + ') ...')) + gists.edit({ + description: description, + public: true, + files: allItems, + id: id + }, (error, result) => { + handleGistResponse(error, result) + if (!error) { + for (const key in allItems) { + if (allItems[key] === null) delete allItems[key] + } + } + }) + } else { + // id is not existing, need to create a new gist + dispatch(displayPopUp('Creating a new gist ...')) + gists.create({ + description: description, + public: true, + files: packaged + }, (error, result) => { + handleGistResponse(error, result) + }) + } + } + } catch (error) { + console.log(error) + displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {}) + } +} + +export const clearPopUp = () => async (dispatch: React.Dispatch) => { + dispatch(hidePopUp()) +} + const fileAdded = async (filePath: string) => { await dispatch(fileAddedSuccess(filePath)) if (filePath.includes('_test.sol')) { diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts index 879de9cf78..01eda80c88 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -13,5 +13,6 @@ export const FileSystemContext = createContext<{ dispatchFetchWorkspaceDirectory: (path: string) => Promise, dispatchSwitchToWorkspace: (name: string) => Promise, dispatchRenameWorkspace: (oldName: string, workspaceName: string) => Promise, - dispatchDeleteWorkspace: (workspaceName: string) => Promise + dispatchDeleteWorkspace: (workspaceName: string) => Promise, + dispatchPublishToGist: (path?: string, type?: string) => Promise }>(null) diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index 86e4b8a5c5..d3d91b53d6 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileSystemContext } from '../contexts' import { browserReducer, browserInitialState } from '../reducers/workspace' -import { initWorkspace, fetchDirectory, addInputField, removeInputField, createWorkspace, fetchWorkspaceDirectory, switchToWorkspace, renameWorkspace, deleteWorkspace } from '../actions/workspace' +import { initWorkspace, fetchDirectory, addInputField, removeInputField, createWorkspace, fetchWorkspaceDirectory, switchToWorkspace, renameWorkspace, deleteWorkspace, clearPopUp, publishToGist } from '../actions/workspace' import { Modal, WorkspaceProps } from '../types' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Workspace } from '../remix-ui-workspace' @@ -62,6 +62,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => { await deleteWorkspace(workspaceName)(fsDispatch) } + const dispatchPublishToGist = async (path?: string, type?: string) => { + await publishToGist(path, type)(fsDispatch) + } + useEffect(() => { if (modals.length > 0) { setFocusModal(() => { @@ -101,6 +105,12 @@ export const FileSystemProvider = (props: WorkspaceProps) => { } }, [fs.notification]) + useEffect(() => { + if (fs.popup) { + toast(fs.popup) + } + }, [fs.popup]) + const handleHideModal = () => { setFocusModal(modal => { return { ...modal, hide: true, message: null } @@ -116,6 +126,7 @@ export const FileSystemProvider = (props: WorkspaceProps) => { const handleToaster = () => { setFocusToaster('') + clearPopUp()(fsDispatch) } const toast = (toasterMsg: string) => { @@ -137,7 +148,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => { dispatchFetchWorkspaceDirectory, dispatchSwitchToWorkspace, dispatchRenameWorkspace, - dispatchDeleteWorkspace + dispatchDeleteWorkspace, + dispatchPublishToGist } return ( diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index 35a5bab34f..acf87a2afc 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -31,7 +31,8 @@ export interface BrowserState { labelOk: string, labelCancel: string }, - readonly: boolean + readonly: boolean, + popup: string } export const browserInitialState: BrowserState = { @@ -61,7 +62,8 @@ export const browserInitialState: BrowserState = { labelOk: '', labelCancel: '' }, - readonly: false + readonly: false, + popup: '' } export const browserReducer = (state = browserInitialState, action: Action) => { @@ -431,6 +433,22 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } + case 'DISPLAY_POPUP_MESSAGE': { + const payload = action.payload as string + + return { + ...state, + popup: payload + } + } + + case 'HIDE_POPUP_MESSAGE': { + return { + ...state, + popup: '' + } + } + default: throw new Error() } diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 79c46e8554..b67a8b5a92 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -231,8 +231,6 @@ export function Workspace (props: WorkspaceProps) { { (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&