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 new file mode 100644 index 0000000000..1bf52d37f4 --- /dev/null +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx @@ -0,0 +1,290 @@ +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({ + menuItems: [ + { + action: 'createNewFile', + title: 'Create New File', + icon: 'fas fa-plus-circle' + }, + { + action: 'publishToGist', + title: 'Publish all [browser] explorer files to a github gist', + icon: 'fab fa-github' + }, + { + action: 'uploadFile', + title: 'Add Local file to the Browser Storage Explorer', + icon: 'far fa-folder-open' + }, + { + action: 'updateGist', + title: 'Update the current [gist] explorer', + icon: 'fab fa-github' + } + ].filter(item => props.menuItems && props.menuItems.find((name) => { return name === item.action })), + actions: {} + }) + + useEffect(() => { + const actions = { + updateGist: () => {}, + uploadFile + } + + setState(prevState => { + return { ...prevState, actions } + }) + }, []) + + const createNewFile = (parentFolder = 'browser/Folder 2') => { + // const self = this + // modalDialogCustom.prompt('Create new file', 'File Name (e.g Untitled.sol)', 'Untitled.sol', (input) => { + // if (!input) input = 'New file' + // get filename from state (state.newFileName) + const fileManager = props.fileManager + const newFileName = parentFolder + '/' + 'unnamed' + Math.floor(Math.random() * 101) + + helper.createNonClashingName(newFileName, props.files, async (error, newName) => { + // if (error) return tooltip('Failed to create file ' + newName + ' ' + error) + if (error) return + const createFile = await fileManager.writeFile(newName, '') + + if (!createFile) { + // tooltip('Failed to create file ' + newName) + } else { + props.addFile(parentFolder, newFileName) + await fileManager.open(newName) + } + }) + // }, null, true) + } + + 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 } + { + state.menuItems.map(({ action, title, icon }, index) => { + if (action === 'uploadFile') { + return ( + + ) + } else { + return ( + { + e.stopPropagation() + if (action === 'createNewFile') { + createNewFile() + } else if (action === 'publishToGist') { + publishToGist() + } else { + state.actions[action]() + } + }} + className={'newFile ' + icon + ' remixui_newFile'} + title={title} + key={index} + > + + ) + } + })} + + + ) +} + +export default FileExplorerMenu 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 6b1882fd7e..ca1e1d8cb9 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx @@ -1,131 +1,33 @@ import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line -import * as async from 'async' -import * as Gists from 'gists' -import * as helper from '../../../../../apps/remix-ide/src/lib/helper' -import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' +import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerProps, File } from './types' import './css/file-explorer.css' -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 FileExplorer = (props: FileExplorerProps) => { const { files, name, registry, plugin } = props - 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 () { - 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 { - // self.events.trigger('focus', [name]) - } - } - fileReader.readAsText(file) - } - const name = files.type + '/' + file.name - - files.exists(name, (error, exist) => { - if (error) console.log(error) - if (!exist) { - loadFile() - } else { - // modalDialogCustom.confirm('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, () => { loadFile() }) - } - }) - }) - } const containerRef = useRef(null) const [state, setState] = useState({ focusElement: [], focusPath: null, - menuItems: [ - { - action: 'createNewFile', - title: 'Create New File', - icon: 'fas fa-plus-circle' - }, - { - action: 'publishToGist', - title: 'Publish all [browser] explorer files to a github gist', - icon: 'fab fa-github' - }, - { - action: 'uploadFile', - title: 'Add Local file to the Browser Storage Explorer', - icon: 'far fa-folder-open' - }, - { - action: 'updateGist', - title: 'Update the current [gist] explorer', - icon: 'fab fa-github' - } - ].filter(item => props.menuItems && props.menuItems.find((name) => { return name === item.action })), files: [], - actions: {}, fileManager: null, - tokenAccess: null, + accessToken: null, ctrlKey: false, newFileName: '' }) useEffect(() => { (async () => { - console.log('registry: ', registry) const fileManager = registry.get('filemanager').api const config = registry.get('config').api - const tokenAccess = config.get('settings/gist-access-token').api + const accessToken = config.get('settings/gist-access-token') const files = await fetchDirectoryContent(name) - const actions = { - updateGist: () => {}, - uploadFile, - publishToGist - } setState(prevState => { - return { ...prevState, fileManager, tokenAccess, files, actions } + return { ...prevState, fileManager, accessToken, files } }) })() }, []) @@ -183,153 +85,35 @@ export const FileExplorer = (props: FileExplorerProps) => { return [...folders, ...files] } - const extractNameFromKey = (key) => { + const extractNameFromKey = (key: string):string => { const keyPath = key.split('/') return keyPath[keyPath.length - 1] } - const createNewFile = (parentFolder = 'browser') => { - // const self = this - // modalDialogCustom.prompt('Create new file', 'File Name (e.g Untitled.sol)', 'Untitled.sol', (input) => { - // if (!input) input = 'New file' - // get filename from state (state.newFileName) - const fileManager = state.fileManager - const newFileName = parentFolder + '/' + 'unnamed' + Math.floor(Math.random() * 101) - - helper.createNonClashingName(newFileName, files, async (error, newName) => { - // if (error) return tooltip('Failed to create file ' + newName + ' ' + error) - if (error) return - const createFile = await fileManager.writeFile(newName, '') - - if (!createFile) { - // tooltip('Failed to create file ' + newName) - } else { - if (parentFolder === name) { - // const updatedFiles = await resolveDirectory(parentFolder, state.files) - - setState(prevState => { - return { - ...prevState, - files: [...prevState.files, { - path: newFileName, - name: extractNameFromKey(newFileName), - isDirectory: false - }] - } - }) - } - await fileManager.open(newName) - if (newName.includes('_test.sol')) { - plugin.events.trigger('newTestFileCreated', [newName]) + const addFile = async (parentFolder: string, newFileName: string) => { + if (parentFolder === name) { + setState(prevState => { + return { + ...prevState, + files: [...prevState.files, { + path: newFileName, + name: extractNameFromKey(newFileName), + isDirectory: false + }], + focusElement: [newFileName] } - } - }) - // }, null, true) - } - - 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() - // ) - } + }) + } else { + const updatedFiles = await resolveDirectory(parentFolder, state.files) - 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')) - } - } + setState(prevState => { + return { ...prevState, files: updatedFiles, focusElement: [newFileName] } + }) } - - /** - * 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 (newFileName.includes('_test.sol')) { + plugin.events.trigger('newTestFileCreated', [newFileName]) } - - // 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(files, folder, (error, packaged) => { - if (error) { - console.log(error) - // modalDialogCustom.alert('Failed to create gist: ' + error.message) - } else { - // check for token - if (!state.tokenAccess) { - // 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: state.tokenAccess }) - - 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) - }) - } - } - } - }) } // self._components = {} @@ -445,52 +229,6 @@ export const FileExplorer = (props: FileExplorerProps) => { } } - const renderMenuItems = () => { - let items - if (state.menuItems) { - items = state.menuItems.map(({ action, title, icon }, index) => { - if (action === 'uploadFile') { - return ( - - ) - } else { - return ( - { - e.stopPropagation() - action === 'createNewFile' ? createNewFile() : state.actions[action]() - }} - className={'newFile ' + icon + ' remixui_newFile'} - title={title} - key={index} - > - - ) - } - }) - } - return ( - <> - { name } - {items} - - ) - } - const renderFiles = (file, index) => { if (file.isDirectory) { return ( @@ -566,7 +304,18 @@ export const FileExplorer = (props: FileExplorerProps) => { }} > - + + } + expand={true}> {(provided) => ( 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 e8cfa73f38..08ba0b9477 100644 --- a/libs/remix-ui/file-explorer/src/lib/types/index.ts +++ b/libs/remix-ui/file-explorer/src/lib/types/index.ts @@ -13,3 +13,12 @@ export interface File { isDirectory: boolean, child?: File[] } + +export interface FileExplorerMenuProps { + title: string, + menuItems: string[], + fileManager: any, + addFile: (parent: string, fileName: string) => void, + files: any, + accessToken: string +}