diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx index 01fb51d6fe..086d9087f9 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx @@ -4,7 +4,7 @@ import { FileExplorerContextMenuProps } from './types' import './css/file-explorer-context-menu.css' export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { - const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pageX, pageY, path, type, ...otherProps } = props + const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, extractParentFromKey, publishToGist, pageX, pageY, path, type, ...otherProps } = props const contextMenuRef = useRef(null) useEffect(() => { @@ -23,7 +23,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => }, [pageX, pageY]) const menu = () => { - return actions.filter(item => item.type.findIndex(name => name === type) !== -1).map((item, index) => { + return actions.filter(item => item.type.findIndex(name => { + if ((name === 'browser/gists') && (type === 'folder') && (extractParentFromKey(path) === name)) return true // add publish to gist for gist folders + return name === type + }) !== -1).map((item, index) => { return
  • case 'Delete': deletePath(path) break + case 'Push changes to gist': + publishToGist() + break default: break } diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx index 44ad137d02..5f3376b057 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx @@ -1,36 +1,5 @@ import React, { useState, useEffect } from 'react' //eslint-disable-line import { FileExplorerMenuProps } from './types' -import * as helper from '../../../../../apps/remix-ide/src/lib/helper' -import * as async from 'async' -import Gists from 'gists' -import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' - -const queryParams = new QueryParams() - -function packageFiles (filesProvider, directory, callback) { - const ret = {} - filesProvider.resolveDirectory(directory, (error, files) => { - if (error) callback(error) - else { - async.eachSeries(Object.keys(files), (path, cb) => { - if (filesProvider.isDirectory(path)) { - cb() - } else { - filesProvider.get(path, (error, content) => { - if (error) return cb(error) - if (/^\s+$/.test(content) || !content.length) { - content = '// this line is added to create a gist. Empty file is not allowed.' - } - ret[path] = { content } - cb() - }) - } - }, (error) => { - callback(error, ret) - }) - } - }) -} export const FileExplorerMenu = (props: FileExplorerMenuProps) => { const [state, setState] = useState({ @@ -66,8 +35,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => { useEffect(() => { const actions = { - updateGist: () => {}, - uploadFile + updateGist: () => {} } setState(prevState => { @@ -75,151 +43,6 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => { }) }, []) - const uploadFile = (target) => { - // TODO The file explorer is merely a view on the current state of - // the files module. Please ask the user here if they want to overwrite - // a file and then just use `files.add`. The file explorer will - // pick that up via the 'fileAdded' event from the files module. - - [...target.files].forEach((file) => { - const files = props.files - - function loadFile (name: string): void { - const fileReader = new FileReader() - - fileReader.onload = async function (event) { - if (helper.checkSpecialChars(file.name)) { - // modalDialogCustom.alert('Special characters are not allowed') - return - } - const success = await files.set(name, event.target.result) - - if (!success) { - // modalDialogCustom.alert('Failed to create file ' + name) - } else { - props.addFile(props.title, name) - await props.fileManager.open(name) - } - } - fileReader.readAsText(file) - } - const name = files.type + '/' + file.name - - files.exists(name, (error, exist) => { - if (error) console.log(error) - if (!exist) { - loadFile(name) - } else { - // modalDialogCustom.confirm('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, () => { loadFile() }) - } - }) - }) - } - - const publishToGist = () => { - // modalDialogCustom.confirm( - // 'Create a public gist', - // 'Are you sure you want to publish all your files in browser directory anonymously as a public gist on github.com? Note: this will not include directories.', - // () => { this.toGist() } - toGist() - // ) - } - - const toGist = (id?: string) => { - const proccedResult = function (error, data) { - if (error) { - // modalDialogCustom.alert('Failed to manage gist: ' + error) - console.log('Failed to manage gist: ' + error) - } else { - if (data.html_url) { - // modalDialogCustom.confirm('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, () => { - // window.open(data.html_url, '_blank') - // }) - } else { - // modalDialogCustom.alert(data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t')) - } - } - } - - /** - * This function is to get the original content of given gist - * @params id is the gist id to fetch - */ - async function getOriginalFiles (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 = id ? 'browser/gists/' + id : 'browser/' - packageFiles(props.files, folder, (error, packaged) => { - if (error) { - console.log(error) - // modalDialogCustom.alert('Failed to create gist: ' + error.message) - } else { - // check for token - if (!props.accessToken) { - // modalDialogCustom.alert( - // 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.' - // ) - } 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: props.accessToken }) - - if (id) { - const originalFileList = 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] - }) - - // tooltip('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 - // tooltip('Creating a new gist ...') - gists.create({ - description: description, - public: true, - files: packaged - }, (error, result) => { - proccedResult(error, result) - }) - } - } - } - }) - } - return ( <> { props.title } @@ -236,7 +59,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => { > { e.stopPropagation() - uploadFile(e.target) + props.uploadFile(e.target) }} multiple /> @@ -253,7 +76,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => { } else if (action === 'createNewFolder') { props.createNewFolder() } else if (action === 'publishToGist') { - publishToGist() + props.publishToGist() } else { state.actions[action]() } 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 42d0106419..d4184671ca 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx @@ -3,13 +3,18 @@ import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // e import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line import { Toaster } from '@remix-ui/toaster' // eslint-disable-line +import * as async from 'async' +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 } from './types' import * as helper from '../../../../../apps/remix-ide/src/lib/helper' +import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' import './css/file-explorer.css' +const queryParams = new QueryParams() + export const FileExplorer = (props: FileExplorerProps) => { const { filesProvider, name, registry, plugin } = props const [state, setState] = useState({ @@ -79,7 +84,7 @@ export const FileExplorer = (props: FileExplorerProps) => { type: ['file', 'folder'] }, { name: 'Push changes to gist', - type: [] + type: ['browser/gists'] }] setState(prevState => { @@ -179,7 +184,7 @@ export const FileExplorer = (props: FileExplorerProps) => { helper.createNonClashingName(newFilePath, filesProvider, async (error, newName) => { if (error) { - return modal('Create File Failed', error, { + modal('Create File Failed', error, { label: 'Close', fn: async () => {} }, null) @@ -249,7 +254,7 @@ export const FileExplorer = (props: FileExplorerProps) => { const exists = fileManager.exists(newPath) if (exists) { - return modal('Rename File Failed', 'File name already exists', { + modal('Rename File Failed', 'File name already exists', { label: 'Close', fn: async () => {} }, null) @@ -518,6 +523,179 @@ export const FileExplorer = (props: FileExplorerProps) => { // } // }) + const uploadFile = (target) => { + // TODO The file explorer is merely a view on the current state of + // the files module. Please ask the user here if they want to overwrite + // a file and then just use `files.add`. The file explorer will + // pick that up via the 'fileAdded' event from the files module. + + [...target.files].forEach((file) => { + const files = props.filesProvider + + const loadFile = (name: string): void => { + const fileReader = new FileReader() + + fileReader.onload = async function (event) { + if (helper.checkSpecialChars(file.name)) { + modal('File Upload Failed', 'Special characters are not allowed', { + label: 'Close', + fn: async () => {} + }, null) + return + } + const success = await files.set(name, event.target.result) + + if (!success) { + modal('File Upload Failed', 'Failed to create file ' + name, { + label: 'Close', + fn: async () => {} + }, null) + } + } + fileReader.readAsText(file) + } + const name = files.type + '/' + file.name + + files.exists(name, (error, exist) => { + if (error) console.log(error) + if (!exist) { + loadFile(name) + } else { + modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, { + label: 'Ok', + fn: () => { + loadFile(name) + } + }, { + label: 'Cancel', + fn: () => {} + }) + } + }) + }) + } + + const publishToGist = () => { + modal('Create a public gist', 'Are you sure you want to publish all your files in browser directory anonymously as a public gist on github.com? Note: this will not include directories.', { + label: 'Ok', + fn: toGist + }, { + label: 'Cancel', + fn: () => {} + }) + } + + const toGist = (id?: string) => { + const proccedResult = function (error, data) { + if (error) { + modal('Publish to gist Failed', 'Failed to manage gist: ' + error, { + label: 'Close', + fn: async () => {} + }, null) + } else { + if (data.html_url) { + modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, { + label: 'Ok', + fn: () => { + window.open(data.html_url, '_blank') + } + }, { + label: 'Cancel', + fn: () => {} + }) + } else { + modal('Publish to gist Failed', data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t'), { + label: 'Close', + fn: async () => {} + }, 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 || [] + } + + // 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 = id ? 'browser/gists/' + id : 'browser/' + + packageFiles(props.filesProvider, folder, async (error, packaged) => { + if (error) { + console.log(error) + modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, { + label: 'Close', + fn: async () => {} + }, null) + } else { + // check for token + if (!state.accessToken) { + modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', { + label: 'Close', + fn: async () => {} + }, 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: state.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] + }) + + // tooltip('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 + // tooltip('Creating a new gist ...') + gists.create({ + description: description, + public: true, + files: packaged + }, (error, result) => { + proccedResult(error, result) + }) + } + } + } + }) + } + const handleHideModal = () => { setState(prevState => { return { ...prevState, modalOptions: { ...state.modalOptions, hide: true } } @@ -754,6 +932,8 @@ export const FileExplorer = (props: FileExplorerProps) => { createNewFolder={handleNewFolderInput} deletePath={deletePath} renamePath={editModeOn} + extractParentFromKey={extractParentFromKey} + publishToGist={publishToGist} pageX={state.focusContext.x} pageY={state.focusContext.y} path={file.path} @@ -811,9 +991,9 @@ export const FileExplorer = (props: FileExplorerProps) => { addFile={addFile} createNewFile={handleNewFileInput} createNewFolder={handleNewFolderInput} - files={filesProvider} + publishToGist={publishToGist} + uploadFile={uploadFile} fileManager={state.fileManager} - accessToken={state.accessToken} /> } expand={true}> @@ -842,3 +1022,28 @@ export const FileExplorer = (props: FileExplorerProps) => { } export default FileExplorer + +function packageFiles (filesProvider, directory, callback) { + const ret = {} + filesProvider.resolveDirectory(directory, (error, files) => { + if (error) callback(error) + else { + async.eachSeries(Object.keys(files), (path, cb) => { + if (filesProvider.isDirectory(path)) { + cb() + } else { + filesProvider.get(path, (error, content) => { + if (error) return cb(error) + if (/^\s+$/.test(content) || !content.length) { + content = '// this line is added to create a gist. Empty file is not allowed.' + } + ret[path] = { content } + cb() + }) + } + }, (error) => { + callback(error, ret) + }) + } + }) +} 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 2e18e13154..06ec2e9fae 100644 --- a/libs/remix-ui/file-explorer/src/lib/types/index.ts +++ b/libs/remix-ui/file-explorer/src/lib/types/index.ts @@ -21,8 +21,8 @@ export interface FileExplorerMenuProps { addFile: (folder: string, fileName: string) => void, createNewFile: (folder?: string) => void, createNewFolder: (parentFolder?: string) => void, - files: any, - accessToken: string + publishToGist: () => void, + uploadFile: (target: EventTarget & HTMLInputElement) => void } export interface FileExplorerContextMenuProps { @@ -32,6 +32,8 @@ export interface FileExplorerContextMenuProps { deletePath: (path: string) => void, renamePath: (path: string, type: string) => void, hideContextMenu: () => void, + extractParentFromKey?: (key: string) => string, + publishToGist?: () => void, pageX: number, pageY: number, path: string,