diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index cb171d7066..93f9d5424f 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -3,7 +3,7 @@ import { ViewPlugin } from '@remixproject/engine-web' import * as packageJson from '../../../../../package.json' import React from 'react' // eslint-disable-line import ReactDOM from 'react-dom' -import { FileSystemProvider, Workspace } from '@remix-ui/workspace' // eslint-disable-line +import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line import { checkSpecialChars, checkSlash } from '../../lib/helper' const { RemixdHandle } = require('../files/remixd-handle.js') const { GitHandle } = require('../files/git-handle.js') @@ -63,15 +63,17 @@ module.exports = class Filepanel extends ViewPlugin { this.appManager = appManager } + onActivation () { + this.renderComponent() + } + render () { return this.el } renderComponent () { ReactDOM.render( - - - + , this.el) } 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 fba220ffce..b03efe9f6b 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx @@ -1,38 +1,26 @@ -import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint-disable-line +import React, { useEffect, useState, useRef, useReducer, useContext } from 'react' // eslint-disable-line // 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 } from './types' +import { FileExplorerProps, File, MenuItems, FileExplorerState } from './types' import { fileSystemReducer, fileSystemInitialState } from './reducers/fileSystem' -import { fetchDirectory, init, resolveDirectory, addInputField, removeInputField } from './actions/fileSystem' +import { init, resolveDirectory, addInputField, removeInputField } from './actions/fileSystem' 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 } = props - const [state, setState] = useState<{ - focusElement: { key: string, type: 'folder' | 'file' | 'gist' }[], - fileManager: any, - ctrlKey: boolean, - newFileName: string, - actions: { id: string, name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], multiselect: boolean, label: string }[], - focusContext: { element: string, x: string, y: string, type: string }, - focusEdit: { element: string, type: string, isNew: boolean, lastEdit: string }, - expandPath: string[], - toasterMsg: string, - mouseOverElement: string, - showContextMenu: boolean, - reservedKeywords: string[], - copyElement: string[] - }>({ + const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems, resetFocus, files } = props + const [state, setState] = useState({ focusElement: [{ key: '', type: 'folder' @@ -40,67 +28,7 @@ export const FileExplorer = (props: FileExplorerProps) => { fileManager: null, ctrlKey: false, newFileName: '', - actions: [{ - id: 'newFile', - name: 'New File', - type: ['folder', 'gist'], - multiselect: false, - label: '' - }, { - id: 'newFolder', - name: 'New Folder', - type: ['folder', 'gist'], - multiselect: false, - label: '' - }, { - id: 'rename', - name: 'Rename', - type: ['file', 'folder'], - multiselect: false, - label: '' - }, { - id: 'delete', - name: 'Delete', - type: ['file', 'folder', 'gist'], - multiselect: false, - label: '' - }, { - id: 'run', - name: 'Run', - extension: ['.js'], - multiselect: false, - label: '' - }, { - id: 'pushChangesToGist', - name: 'Push changes to gist', - type: ['gist'], - multiselect: false, - label: '' - }, { - id: 'publishFolderToGist', - name: 'Publish folder to gist', - type: ['folder'], - multiselect: false, - label: '' - }, { - id: 'publishFileToGist', - name: 'Publish file to gist', - type: ['file'], - multiselect: false, - label: '' - }, { - id: 'copy', - name: 'Copy', - type: ['folder', 'file'], - multiselect: false, - label: '' - }, { - id: 'deleteAll', - name: 'Delete All', - type: ['folder', 'file'], - multiselect: true, - label: '' - }], + actions: contextMenuActions, focusContext: { element: null, x: null, @@ -123,22 +51,15 @@ export const FileExplorer = (props: FileExplorerProps) => { const [canPaste, setCanPaste] = useState(false) const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState) const editRef = useRef(null) + const global = useContext(FileSystemContext) useEffect(() => { init(props.filesProvider, props.plugin, props.registry)(dispatch) }, []) - useEffect(() => { - const provider = fileSystem.provider.provider - - if (provider) { - fetchDirectory(provider, props.name)(dispatch) - } - }, [fileSystem.provider.provider, props.name]) - useEffect(() => { if (fileSystem.notification.message) { - modal(fileSystem.notification.title, fileSystem.notification.message, fileSystem.notification.labelOk, fileSystem.notification.actionOk, fileSystem.notification.labelCancel, fileSystem.notification.actionCancel) + global.modal(fileSystem.notification.title, fileSystem.notification.message, fileSystem.notification.labelOk, fileSystem.notification.actionOk, fileSystem.notification.labelCancel, fileSystem.notification.actionCancel) } }, [fileSystem.notification.message]) @@ -317,7 +238,7 @@ export const FileExplorer = (props: FileExplorerProps) => { }) } } catch (error) { - return modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {}) + return global.modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {}) } } @@ -329,14 +250,14 @@ export const FileExplorer = (props: FileExplorerProps) => { const exists = await fileManager.exists(dirName) if (exists) { - return modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, 'Close', () => {}) + return global.modal('Rename File Failed', `A file or folder ${extractNameFromKey(newFolderPath)} already exists at this location. Please choose a different name.`, 'Close', () => {}) } await fileManager.mkdir(dirName) setState(prevState => { return { ...prevState, focusElement: [{ key: newFolderPath, type: 'folder' }] } }) } catch (e) { - return modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {}) + return global.modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {}) } } @@ -348,7 +269,7 @@ export const FileExplorer = (props: FileExplorerProps) => { return toast('cannot delete file. ' + name + ' is a read only explorer') } } - modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', async () => { + global.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', async () => { const fileManager = state.fileManager for (const p of path) { try { @@ -367,12 +288,12 @@ export const FileExplorer = (props: FileExplorerProps) => { const exists = await fileManager.exists(newPath) if (exists) { - modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', () => {}) + global.modal('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', () => {}) } else { await fileManager.rename(oldPath, newPath) } } catch (error) { - modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {}) + global.modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {}) } } @@ -395,13 +316,13 @@ export const FileExplorer = (props: FileExplorerProps) => { fileReader.onload = async function (event) { if (helper.checkSpecialChars(file.name)) { - modal('File Upload Failed', 'Special characters are not allowed', 'Close', async () => {}) + global.modal('File Upload Failed', 'Special characters are not allowed', 'Close', async () => {}) return } const success = await filesProvider.set(name, event.target.result) if (!success) { - return modal('File Upload Failed', 'Failed to create file ' + name, 'Close', async () => {}) + return global.modal('File Upload Failed', 'Failed to create file ' + name, 'Close', async () => {}) } const config = registry.get('config').api const editor = registry.get('editor').api @@ -418,7 +339,7 @@ export const FileExplorer = (props: FileExplorerProps) => { if (!exist) { loadFile(name) } else { - modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', () => { + global.modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', () => { loadFile(name) }, 'Cancel', () => {}) } @@ -449,35 +370,35 @@ export const FileExplorer = (props: FileExplorerProps) => { } const publishToGist = (path?: string, type?: string) => { - modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) + global.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) } const pushChangesToGist = (path?: string, type?: string) => { - modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {}) + global.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {}) } const publishFolderToGist = (path?: string, type?: string) => { - modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) + global.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) } const publishFileToGist = (path?: string, type?: string) => { - modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) + global.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) } const toGist = (path?: string, type?: string) => { const filesProvider = fileSystem.provider.provider const proccedResult = function (error, data) { if (error) { - modal('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', () => {}) + global.modal('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', () => {}) } 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?`, 'OK', () => { + 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 - modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', () => {}) + global.modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', () => {}) } } } @@ -504,14 +425,14 @@ export const FileExplorer = (props: FileExplorerProps) => { packageFiles(filesProvider, folder, async (error, packaged) => { if (error) { console.log(error) - modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', async () => {}) + 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) { - modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', () => {}) + 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=' @@ -583,7 +504,7 @@ export const FileExplorer = (props: FileExplorerProps) => { }) } - const handleClickFile = (path: string, type: string) => { + const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => { path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path if (!state.ctrlKey) { state.fileManager.open(path) @@ -606,7 +527,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } } - const handleClickFolder = async (path: string, type: string) => { + const handleClickFolder = async (path: string, type: 'folder' | 'file' | 'gist') => { if (state.ctrlKey) { if (state.focusElement.findIndex(item => item.key === path) !== -1) { setState(prevState => { @@ -687,12 +608,12 @@ export const FileExplorer = (props: FileExplorerProps) => { }) } if (helper.checkSpecialChars(content)) { - modal('Validation Error', 'Special characters are not allowed', 'OK', () => {}) + global.modal('Validation Error', 'Special characters are not allowed', 'OK', () => {}) } else { if (state.focusEdit.isNew) { if (hasReservedKeyword(content)) { removeInputField(parentFolder)(dispatch) - modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) + global.modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) } else { state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) removeInputField(parentFolder)(dispatch) @@ -700,7 +621,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } else { if (hasReservedKeyword(content)) { editRef.current.textContent = state.focusEdit.lastEdit - modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) + global.modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) } else { const oldPath: string = state.focusEdit.element const oldName = extractNameFromKey(oldPath) @@ -759,7 +680,7 @@ export const FileExplorer = (props: FileExplorerProps) => { }) } - const handleCopyClick = (path: string, type: string) => { + const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file') => { setState(prevState => { return { ...prevState, copyElement: [{ key: path, type }] } }) @@ -927,8 +848,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
{ - fileSystem.files.files[props.name] && Object.keys(fileSystem.files.files[props.name]).map((key, index) => { - return renderFiles(fileSystem.files.files[props.name][key], index) + files[props.name] && Object.keys(files[props.name]).map((key, index) => { + return renderFiles(files[props.name][key], index) }) } 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 a0e5ce912a..c1f8221688 100644 --- a/libs/remix-ui/file-explorer/src/lib/types/index.ts +++ b/libs/remix-ui/file-explorer/src/lib/types/index.ts @@ -13,14 +13,15 @@ export interface FileExplorerProps { removedContextMenuItems: MenuItems, displayInput?: boolean, externalUploads?: EventTarget & HTMLInputElement, - resetFocus?: (value: boolean) => void + resetFocus?: (value: boolean) => void, + files: { [x: string]: Record } } export interface File { path: string, name: string, isDirectory: boolean, - type: string, + type: 'folder' | 'file' | 'gist', child?: File[] } @@ -34,7 +35,7 @@ export interface FileExplorerMenuProps { uploadFile: (target: EventTarget & HTMLInputElement) => void } -export type action = { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string, multiselect: boolean, label: string } +export type action = { name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string } export interface FileExplorerContextMenuProps { actions: action[], @@ -58,3 +59,44 @@ export interface FileExplorerContextMenuProps { copy?: (path: string, type: string) => void, paste?: (destination: string, type: string) => void } + +export interface FileExplorerState { + focusElement: { + key: string + type: 'folder' | 'file' | 'gist' + }[] + fileManager: any + ctrlKey: boolean + newFileName: string + actions: { + id: string + name: string + type?: Array<'folder' | 'gist' | 'file'> + path?: string[] + extension?: string[] + pattern?: string[] + multiselect: boolean + label: string + }[] + focusContext: { + element: string + x: number + y: number + type: string + } + focusEdit: { + element: string + type: string + isNew: boolean + lastEdit: string + } + expandPath: string[] + toasterMsg: string + mouseOverElement: string + showContextMenu: boolean + reservedKeywords: string[] + copyElement: { + key: string + type: 'folder' | 'gist' | 'file' + }[] + } diff --git a/libs/remix-ui/file-explorer/src/lib/utils/index.ts b/libs/remix-ui/file-explorer/src/lib/utils/index.ts index 3db8f00902..56a7453de9 100644 --- a/libs/remix-ui/file-explorer/src/lib/utils/index.ts +++ b/libs/remix-ui/file-explorer/src/lib/utils/index.ts @@ -1,3 +1,5 @@ +import { MenuItems } from '../types' + export const extractNameFromKey = (key: string): string => { const keyPath = key.split('/') @@ -11,3 +13,65 @@ export const extractParentFromKey = (key: string):string => { return keyPath.join('/') } + +export const contextMenuActions: MenuItems = [{ + id: 'newFile', + name: 'New File', + type: ['folder', 'gist'], + multiselect: false, + label: '' +}, { + id: 'newFolder', + name: 'New Folder', + type: ['folder', 'gist'], + multiselect: false, + label: '' +}, { + id: 'rename', + name: 'Rename', + type: ['file', 'folder'], + multiselect: false, + label: '' +}, { + id: 'delete', + name: 'Delete', + type: ['file', 'folder', 'gist'], + multiselect: false, + label: '' +}, { + id: 'run', + name: 'Run', + extension: ['.js'], + multiselect: false, + label: '' +}, { + id: 'pushChangesToGist', + name: 'Push changes to gist', + type: ['gist'], + multiselect: false, + label: '' +}, { + id: 'publishFolderToGist', + name: 'Publish folder to gist', + type: ['folder'], + multiselect: false, + label: '' +}, { + id: 'publishFileToGist', + name: 'Publish file to gist', + type: ['file'], + multiselect: false, + label: '' +}, { + id: 'copy', + name: 'Copy', + type: ['folder', 'file'], + multiselect: false, + label: '' +}, { + id: 'deleteAll', + name: 'Delete All', + type: ['folder', 'file'], + multiselect: true, + label: '' +}] diff --git a/libs/remix-ui/helper/src/index.ts b/libs/remix-ui/helper/src/index.ts index 2c74e0a913..961f14639b 100644 --- a/libs/remix-ui/helper/src/index.ts +++ b/libs/remix-ui/helper/src/index.ts @@ -1 +1 @@ -export * from './lib/remix-ui-helper'; +export * from './lib/remix-ui-helper' diff --git a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts index e1ba870471..fa7e98f49f 100644 --- a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts +++ b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts @@ -1,3 +1,21 @@ -export function remixUiHelper(): string { - return 'remix-ui-helper'; +export const extractNameFromKey = (key: string): string => { + const keyPath = key.split('/') + + return keyPath[keyPath.length - 1] +} + +export const extractParentFromKey = (key: string):string => { + if (!key) return + const keyPath = key.split('/') + keyPath.pop() + + return keyPath.join('/') +} + +export const checkSpecialChars = (name: string) => { + return name.match(/[:*?"<>\\'|]/) != null +} + +export const checkSlash = (name: string) => { + return name.match(/\//) != null } diff --git a/libs/remix-ui/workspace/src/index.ts b/libs/remix-ui/workspace/src/index.ts index 51159e9b18..166467d115 100644 --- a/libs/remix-ui/workspace/src/index.ts +++ b/libs/remix-ui/workspace/src/index.ts @@ -1,2 +1,2 @@ -export * from './lib/remix-ui-workspace' export * from './lib/providers/FileSystemProvider' +export * from './lib/contexts' diff --git a/libs/remix-ui/workspace/src/lib/actions/gist-handler.ts b/libs/remix-ui/workspace/src/lib/actions/gist-handler.ts deleted file mode 100644 index 5df4ad76c7..0000000000 --- a/libs/remix-ui/workspace/src/lib/actions/gist-handler.ts +++ /dev/null @@ -1,55 +0,0 @@ -// var modalDialogCustom = require('../app/ui/modal-dialog-custom') -import * as request from 'request' - -export class GistHandler { - handleLoad (params) { - let loadingFromGist = false - let gistId - - if (params.gist) { - loadingFromGist = true - } else { - gistId = params.gist - loadingFromGist = !!gistId - } - - return gistId - } - - getGistId (str: string) { - const idr = /[0-9A-Fa-f]{8,}/ - const match = idr.exec(str) - - return match ? match[0] : null - } - - loadFromGist (params, fileManager) { - const gistId = this.handleLoad(params) - - request.get({ - url: `https://api.github.com/gists/${gistId}`, - json: true - }, async (error, response, data = {}) => { - if (error || !data.files) { - // modalDialogCustom.alert('Gist load error', error || data.message) - return - } - const obj = {} - - Object.keys(data.files).forEach((element) => { - const path = element.replace(/\.\.\./g, '/') - - obj['/' + 'gist-' + gistId + '/' + path] = data.files[element] - }) - fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => { - if (!errorLoadingFile) { - const provider = fileManager.getProvider('workspace') - - provider.lastLoadedGistId = gistId - } else { - // modalDialogCustom.alert('Gist load error', errorLoadingFile.message || errorLoadingFile) - } - }) - }) - } -} diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index 95fe8b5480..f8c39e8914 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -1,8 +1,8 @@ -import { bufferToHex, keccakFromString } from 'ethereumjs-util' -import { checkSpecialChars, checkSlash } from '../../../../../../apps/remix-ide/src/lib/helper' import React from 'react' +import { bufferToHex, keccakFromString } from 'ethereumjs-util' +import axios, { AxiosResponse } from 'axios' +import { checkSpecialChars, checkSlash } from '@remix-ui/helper' -// const GistHandler = require('../../../../../../apps/remix-ide/src/lib/gist-handler') const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples') // const queuedEvents = [] @@ -51,6 +51,19 @@ const fetchDirectorySuccess = (path: string, files) => { } } +export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => { + return { + type: 'DISPLAY_NOTIFICATION', + payload: { title, message, labelOk, labelCancel, actionOk, actionCancel } + } +} + +export const hideNotification = () => { + return { + type: 'DISPLAY_NOTIFICATION' + } +} + export const fetchDirectory = (mode: 'browser' | 'localhost', path: string) => (dispatch: React.Dispatch) => { const provider = mode === 'browser' ? plugin.fileProviders.workspace : plugin.fileProviders.localhost const promise = new Promise((resolve) => { @@ -73,34 +86,75 @@ export const fetchDirectory = (mode: 'browser' | 'localhost', path: string) => ( 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') - if (await workspaceExists(workspaceName)) throw new Error('workspace already exists') + if (await workspaceExists(workspaceName) && template === 'default-template') throw new Error('workspace already exists') else { const workspaceProvider = plugin.fileProviders.workspace await workspaceProvider.createWorkspace(workspaceName) if (setDefaults) { + const queryParams = new QueryParams() + const params = queryParams.get() + switch (template) { case 'code-template': // creates a new workspace code-sample and loads code from url params. try { - const queryParams = new QueryParams() - const params = queryParams.get() - await workspaceProvider.createWorkspace(workspaceName) + let path = ''; let content = '' + + if (params.code) { + const hash = bufferToHex(keccakFromString(params.code)) - const hash = bufferToHex(keccakFromString(params.code)) - const fileName = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol' - const path = fileName + path = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol' + content = atob(params.code) + await workspaceProvider.set(path, content) + } else if (params.url) { + const data = await plugin.call('contentImport', 'resolve', params.url) - await workspaceProvider.set(path, atob(params.code)) - await plugin.fileManager.openFile(fileName) + path = data.cleanUrl + content = data.content + await workspaceProvider.set(path, content) + } + await plugin.fileManager.openFile(path) } catch (e) { console.error(e) } break + case 'gist-template': // creates a new workspace gist-sample and get the file from gist + try { + const gistId = params.gist + const response: AxiosResponse = await axios.get(`https://api.github.com/gists/${gistId}`) + const data = response.data + + console.log('data: ', data) + if (!data.files) { + dispatch(displayNotification('Gist load error', 'No files found', 'OK', null, () => {}, null)) + return + } + // const obj = {} + + // Object.keys(data.files).forEach((element) => { + // const path = element.replace(/\.\.\./g, '/') + + // obj['/' + 'gist-' + gistId + '/' + path] = data.files[element] + // }) + // fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => { + // if (!errorLoadingFile) { + // const provider = fileManager.getProvider('workspace') + + // provider.lastLoadedGistId = gistId + // } else { + // // modalDialogCustom.alert('', errorLoadingFile.message || errorLoadingFile) + // } + // }) + } catch (e) { + dispatch(displayNotification('Gist load error', e.message, 'OK', null, () => {}, null)) + console.error(e) + } break + case 'default-template': // creates a new workspace and populates it with default project template. // insert example contracts @@ -150,29 +204,17 @@ const getWorkspaces = async (): Promise | undefined => { export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch) => { if (filePanelPlugin) { - console.log('filePanelPlugin: ', filePanelPlugin) plugin = filePanelPlugin dispatch = reducerDispatch const provider = filePanelPlugin.fileProviders.workspace const queryParams = new QueryParams() - // const gistHandler = new GistHandler() const params = queryParams.get() - // let loadedFromGist = false const workspaces = await getWorkspaces() || [] - // if (params.gist) { - // initialWorkspace = 'gist-sample' - // await provider.createWorkspace(initialWorkspace) - // loadedFromGist = gistHandler.loadFromGist(params, plugin.fileManager) - // } - // if (loadedFromGist) { - // dispatch(setWorkspaces(workspaces)) - // dispatch(setCurrentWorkspace(initialWorkspace)) - // return - // } if (params.gist) { - - } else if (params.code) { + await createWorkspaceTemplate('gist-sample', true, 'gist-template') + dispatch(setCurrentWorkspace('gist-sample')) + } else if (params.code || params.url) { await createWorkspaceTemplate('code-sample', true, 'code-template') dispatch(setCurrentWorkspace('code-sample')) } else { @@ -225,7 +267,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React. // provider.event.on('createWorkspace', (name) => { // createNewWorkspace(name) // }) - dispatch(setWorkspaces(workspaces)) + // dispatch(setWorkspaces(workspaces)) dispatch(setMode('browser')) } } diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index 40f1c18376..7c6d30e1fa 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -1,13 +1,16 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars import React, { useReducer, useState, useEffect } from 'react' +import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileSystemContext } from '../contexts' import { browserReducer, browserInitialState } from '../reducers/workspace' import { initWorkspace, initLocalhost, fetchDirectory } from '../actions/workspace' -import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line -import { Modal } from '../types' +import { Modal, WorkspaceProps } from '../types' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Workspace } from '../remix-ui-workspace' -export const FileSystemProvider = ({ filePanel, children }) => { +export const FileSystemProvider = (props: WorkspaceProps) => { + const { plugin } = props const [fs, fsDispatch] = useReducer(browserReducer, browserInitialState) const [focusModal, setFocusModal] = useState({ hide: true, @@ -21,11 +24,11 @@ export const FileSystemProvider = ({ filePanel, children }) => { const [modals, setModals] = useState([]) const dispatchInitWorkspace = async () => { - await initWorkspace(filePanel)(fsDispatch) + await initWorkspace(plugin)(fsDispatch) } const dispatchInitLocalhost = async () => { - await initLocalhost(filePanel)(fsDispatch) + await initLocalhost(plugin)(fsDispatch) } const dispatchFetchDirectory = async (path: string) => { @@ -34,7 +37,7 @@ export const FileSystemProvider = ({ filePanel, children }) => { useEffect(() => { if (modals.length > 0) { - setModals(modals => { + setFocusModal(() => { const focusModal = { hide: false, title: modals[0].title, @@ -44,14 +47,12 @@ export const FileSystemProvider = ({ filePanel, children }) => { cancelLabel: modals[0].cancelLabel, cancelFn: modals[0].cancelFn } - - modals.shift() - return { - ...modals, - focusModal, - modals: modals - } + return focusModal }) + const modalList = modals.slice() + + modalList.shift() + setModals(modalList) } }, [modals]) @@ -62,7 +63,10 @@ export const FileSystemProvider = ({ filePanel, children }) => { } const modal = (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => { - setModals(modals => [...modals, { message, title, okLabel, okFn, cancelLabel, cancelFn }]) + setModals(modals => { + modals.push({ message, title, okLabel, okFn, cancelLabel, cancelFn }) + return [...modals] + }) } const value = { @@ -74,7 +78,7 @@ export const FileSystemProvider = ({ filePanel, children }) => { } return ( - { children } + ) diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index a4183f4254..f6057aac86 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -1,4 +1,4 @@ -import { extractNameFromKey } from '@remix-ui/file-explorer' +import { extractNameFromKey, File } from '@remix-ui/file-explorer' interface Action { type: string payload: any @@ -7,36 +7,52 @@ export interface BrowserState { browser: { currentWorkspace: string, workspaces: string[], - files: [] + files: { [x: string]: Record } isRequesting: boolean, isSuccessful: boolean, error: string }, localhost: { - files: [], + files: { [x: string]: Record }, isRequesting: boolean, isSuccessful: boolean, error: string }, - mode: 'browser' | 'localhost' + mode: 'browser' | 'localhost', + notification: { + title: string, + message: string, + actionOk: () => void, + actionCancel: (() => void) | null, + labelOk: string, + labelCancel: string + } } export const browserInitialState: BrowserState = { browser: { currentWorkspace: '', workspaces: [], - files: [], + files: {}, isRequesting: false, isSuccessful: false, error: null }, localhost: { - files: [], + files: {}, isRequesting: false, isSuccessful: false, error: null }, - mode: 'browser' + mode: 'browser', + notification: { + title: '', + message: '', + actionOk: () => {}, + actionCancel: () => {}, + labelOk: '', + labelCancel: '' + } } export const browserReducer = (state = browserInitialState, action: Action) => { @@ -67,9 +83,11 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } case 'SET_MODE': { + const payload = action.payload as 'browser' | 'localhost' + return { ...state, - mode: action.payload + mode: payload } } @@ -84,6 +102,7 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } } + case 'FETCH_DIRECTORY_SUCCESS': { const payload = action.payload as { path: string, files } @@ -98,6 +117,7 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } } + case 'FETCH_DIRECTORY_ERROR': { return { ...state, @@ -109,6 +129,29 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } } + + case 'DISPLAY_NOTIFICATION': { + const payload = action.payload as { title: string, message: string, actionOk: () => void, actionCancel: () => void, labelOk: string, labelCancel: string } + + return { + ...state, + notification: { + title: payload.title, + message: payload.message, + actionOk: payload.actionOk || browserInitialState.notification.actionOk, + actionCancel: payload.actionCancel || browserInitialState.notification.actionCancel, + labelOk: payload.labelOk, + labelCancel: payload.labelCancel + } + } + } + + case 'HIDE_NOTIFICATION': { + return { + ...state, + notification: browserInitialState.notification + } + } default: throw new Error() } @@ -120,7 +163,7 @@ const fetchDirectoryContent = (fileTree, folderPath: string) => { return { [extractNameFromKey(folderPath)]: files } } -const normalize = (filesList): any => { +const normalize = (filesList): Record => { const folders = {} const files = {} 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 4f4f26493d..f09e111c0a 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -11,7 +11,6 @@ export function Workspace (props: WorkspaceProps) { const LOCALHOST = ' - connect to localhost - ' const NO_WORKSPACE = ' - none - ' const [state, setState] = useState({ - workspaces: [], reset: false, hideRemixdExplorer: true, displayNewFile: false, @@ -28,7 +27,10 @@ export function Workspace (props: WorkspaceProps) { }, []) useEffect(() => { - if (global.fs.browser.currentWorkspace) setCurrentWorkspace(global.fs.browser.currentWorkspace) + if (global.fs.browser.currentWorkspace) { + setCurrentWorkspace(global.fs.browser.currentWorkspace) + global.dispatchFetchDirectory(global.fs.browser.currentWorkspace) + } }, [global.fs.browser.currentWorkspace]) props.plugin.resetNewFile = () => { @@ -46,18 +48,18 @@ export function Workspace (props: WorkspaceProps) { return setWorkspace(workspaceName) } - props.plugin.request.createNewFile = async () => { - if (!state.workspaces.length) await createNewWorkspace('default_workspace') - props.plugin.resetNewFile() - } + // props.plugin.request.createNewFile = async () => { + // if (!state.workspaces.length) await createNewWorkspace('default_workspace') + // props.plugin.resetNewFile() + // } - props.plugin.request.uploadFile = async (target: EventTarget & HTMLInputElement) => { - if (!state.workspaces.length) await createNewWorkspace('default_workspace') + // props.plugin.request.uploadFile = async (target: EventTarget & HTMLInputElement) => { + // if (!state.workspaces.length) await createNewWorkspace('default_workspace') - setState(prevState => { - return { ...prevState, uploadFileEvent: target } - }) - } + // setState(prevState => { + // return { ...prevState, uploadFileEvent: target } + // }) + // } props.plugin.request.getCurrentWorkspace = () => { return { name: currentWorkspace, isLocalhost: currentWorkspace === LOCALHOST, absolutePath: `${props.plugin.workspace.workspacesPath}/${currentWorkspace}` } @@ -287,6 +289,7 @@ export function Workspace (props: WorkspaceProps) { displayInput={state.displayNewFile} externalUploads={state.uploadFileEvent} resetFocus={resetFocus} + files={global.fs.browser.files} /> }
@@ -304,6 +307,7 @@ export function Workspace (props: WorkspaceProps) { contextMenuItems={props.plugin.registeredMenuItems} removedContextMenuItems={props.plugin.removedMenuItems} resetFocus={resetFocus} + files={global.fs.localhost.files} /> } diff --git a/libs/remix-ui/workspace/src/lib/types/index.ts b/libs/remix-ui/workspace/src/lib/types/index.ts index caca5a1856..998b7cd929 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -1,5 +1,4 @@ import { MenuItems } from '@remix-ui/file-explorer' - export interface WorkspaceProps { plugin: { setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void, @@ -29,7 +28,6 @@ export interface WorkspaceProps { } } export interface WorkspaceState { - workspaces: string[] reset: boolean hideRemixdExplorer: boolean displayNewFile: boolean @@ -48,3 +46,11 @@ export interface Modal { cancelLabel: string cancelFn: () => void } + +export interface File { + path: string, + name: string, + isDirectory: boolean, + type: 'folder' | 'file' | 'gist', + child?: File[] +}