From da9207ddd78cc93cd51b4bd89bcd13f0f5dba524 Mon Sep 17 00:00:00 2001 From: ioedeveloper Date: Wed, 11 Aug 2021 18:03:08 +0100 Subject: [PATCH] Fetch directory from global FS --- apps/remix-ide/src/app/panels/file-panel.js | 6 +- libs/remix-ui/file-explorer/src/index.ts | 1 + .../file-explorer/src/lib/file-explorer.tsx | 120 +------ libs/remix-ui/helper/.eslintrc | 1 + libs/remix-ui/helper/README.md | 3 + libs/remix-ui/helper/src/index.ts | 1 + .../helper/src/lib/remix-ui-helper.ts | 3 + libs/remix-ui/helper/tsconfig.json | 10 + libs/remix-ui/helper/tsconfig.lib.json | 12 + .../modal-dialog/src/lib/types/index.ts | 2 +- libs/remix-ui/workspace/src/index.ts | 1 + .../workspace/src/lib/actions/workspace.ts | 313 ++++++++++++++---- .../workspace/src/lib/contexts/index.ts | 10 + .../src/lib/providers/FileSystemProvider.tsx | 83 +++++ .../workspace/src/lib/reducers/workspace.ts | 136 +++++++- .../workspace/src/lib/remix-ui-workspace.tsx | 68 +--- .../remix-ui/workspace/src/lib/types/index.ts | 3 +- nx.json | 3 + tsconfig.base.json | 5 +- workspace.json | 16 + 20 files changed, 565 insertions(+), 232 deletions(-) create mode 100644 libs/remix-ui/helper/.eslintrc create mode 100644 libs/remix-ui/helper/README.md create mode 100644 libs/remix-ui/helper/src/index.ts create mode 100644 libs/remix-ui/helper/src/lib/remix-ui-helper.ts create mode 100644 libs/remix-ui/helper/tsconfig.json create mode 100644 libs/remix-ui/helper/tsconfig.lib.json create mode 100644 libs/remix-ui/workspace/src/lib/contexts/index.ts create mode 100644 libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 997f9f35ec..cb171d7066 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 { Workspace } from '@remix-ui/workspace' // eslint-disable-line +import { FileSystemProvider, Workspace } 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') @@ -69,7 +69,9 @@ module.exports = class Filepanel extends ViewPlugin { renderComponent () { ReactDOM.render( - + + + , this.el) } diff --git a/libs/remix-ui/file-explorer/src/index.ts b/libs/remix-ui/file-explorer/src/index.ts index d6129e7e11..81d8437a71 100644 --- a/libs/remix-ui/file-explorer/src/index.ts +++ b/libs/remix-ui/file-explorer/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/file-explorer' export * from './lib/types' +export * from './lib/utils' 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 836adc7777..fba220ffce 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState, useRef, useReducer } 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 { ModalDialog } from '@remix-ui/modal-dialog' // 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 @@ -19,12 +18,25 @@ const queryParams = new QueryParams() export const FileExplorer = (props: FileExplorerProps) => { const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems, resetFocus } = props - const [state, setState] = useState({ + 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[] + }>({ focusElement: [{ key: '', type: 'folder' }], - files: [], fileManager: null, ctrlKey: false, newFileName: '', @@ -32,90 +44,60 @@ export const FileExplorer = (props: FileExplorerProps) => { id: 'newFile', name: 'New File', type: ['folder', 'gist'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'newFolder', name: 'New Folder', type: ['folder', 'gist'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'rename', name: 'Rename', type: ['file', 'folder'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'delete', name: 'Delete', type: ['file', 'folder', 'gist'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'run', name: 'Run', - type: [], - path: [], extension: ['.js'], - pattern: [], multiselect: false, label: '' }, { id: 'pushChangesToGist', name: 'Push changes to gist', type: ['gist'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'publishFolderToGist', name: 'Publish folder to gist', type: ['folder'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'publishFileToGist', name: 'Publish file to gist', type: ['file'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'copy', name: 'Copy', type: ['folder', 'file'], - path: [], - extension: [], - pattern: [], multiselect: false, label: '' }, { id: 'deleteAll', name: 'Delete All', type: ['folder', 'file'], - path: [], - extension: [], - pattern: [], multiselect: true, label: '' }], @@ -132,17 +114,6 @@ export const FileExplorer = (props: FileExplorerProps) => { lastEdit: '' }, expandPath: [name], - focusModal: { - hide: true, - title: '', - message: '', - okLabel: '', - okFn: () => {}, - cancelLabel: '', - cancelFn: () => {}, - handleHide: null - }, - modals: [], toasterMsg: '', mouseOverElement: null, showContextMenu: false, @@ -233,30 +204,6 @@ export const FileExplorer = (props: FileExplorerProps) => { } }, [externalUploads]) - useEffect(() => { - if (state.modals.length > 0) { - setState(prevState => { - const focusModal = { - hide: false, - title: prevState.modals[0].title, - message: prevState.modals[0].message, - okLabel: prevState.modals[0].okLabel, - okFn: prevState.modals[0].okFn, - cancelLabel: prevState.modals[0].cancelLabel, - cancelFn: prevState.modals[0].cancelFn, - handleHide: prevState.modals[0].handleHide - } - - prevState.modals.shift() - return { - ...prevState, - focusModal, - modals: prevState.modals - } - }) - } - }, [state.modals]) - useEffect(() => { const keyPressHandler = (e: KeyboardEvent) => { if (e.shiftKey) { @@ -630,30 +577,6 @@ export const FileExplorer = (props: FileExplorerProps) => { plugin.call(cmd.id, cmd.name, cmd) } - const handleHideModal = () => { - setState(prevState => { - return { ...prevState, focusModal: { ...state.focusModal, hide: true } } - }) - } - // eslint-disable-next-line no-undef - const modal = (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => { - setState(prevState => { - return { - ...prevState, - modals: [...prevState.modals, - { - message, - title, - okLabel, - okFn, - cancelLabel, - cancelFn, - handleHide: handleHideModal - }] - } - }) - } - const toast = (message: string) => { setState(prevState => { return { ...prevState, toasterMsg: message } @@ -1012,19 +935,6 @@ export const FileExplorer = (props: FileExplorerProps) => { - { - props.name && - } { state.showContextMenu && void, modalClass?: string, showCancelIcon?: boolean, - hide: boolean, + hide?: boolean, handleHide: (hideState?: boolean) => void, children?: React.ReactNode } diff --git a/libs/remix-ui/workspace/src/index.ts b/libs/remix-ui/workspace/src/index.ts index 97b8a61e38..51159e9b18 100644 --- a/libs/remix-ui/workspace/src/index.ts +++ b/libs/remix-ui/workspace/src/index.ts @@ -1 +1,2 @@ export * from './lib/remix-ui-workspace' +export * from './lib/providers/FileSystemProvider' diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index 8ede21ac4b..95fe8b5480 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -1,10 +1,13 @@ import { bufferToHex, keccakFromString } from 'ethereumjs-util' import { checkSpecialChars, checkSlash } from '../../../../../../apps/remix-ide/src/lib/helper' +import React from 'react' // 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') -let plugin +// const queuedEvents = [] +// const pendingEvents = {} +let plugin, dispatch: React.Dispatch const setCurrentWorkspace = (workspace: string) => { return { @@ -20,6 +23,53 @@ const setWorkspaces = (workspaces: string[]) => { } } +const setMode = (mode: 'browser' | 'localhost') => { + return { + type: 'SET_MODE', + payload: mode + } +} + +const fetchDirectoryError = (error: any) => { + return { + type: 'FETCH_DIRECTORY_ERROR', + payload: error + } +} + +const fetchDirectoryRequest = (promise: Promise) => { + return { + type: 'FETCH_DIRECTORY_REQUEST', + payload: promise + } +} + +const fetchDirectorySuccess = (path: string, files) => { + return { + type: 'FETCH_DIRECTORY_SUCCESS', + payload: { path, files } + } +} + +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) => { + provider.resolveDirectory(path, (error, fileTree) => { + if (error) console.error(error) + + resolve(fileTree) + }) + }) + + dispatch(fetchDirectoryRequest(promise)) + promise.then((fileTree) => { + dispatch(fetchDirectorySuccess(path, fileTree)) + }).catch((error) => { + dispatch(fetchDirectoryError({ error })) + }) + return promise +} + 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') @@ -35,12 +85,14 @@ const createWorkspaceTemplate = async (workspaceName: string, setDefaults = true try { const queryParams = new QueryParams() const params = queryParams.get() - await plugin.fileProviders.worspace.createWorkspace(workspaceName) + + await workspaceProvider.createWorkspace(workspaceName) + const hash = bufferToHex(keccakFromString(params.code)) const fileName = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol' const path = fileName - await plugin.fileProviders.workspace.set(path, atob(params.code)) + await workspaceProvider.set(path, atob(params.code)) await plugin.fileManager.openFile(fileName) } catch (e) { console.error(e) @@ -73,63 +125,6 @@ const workspaceExists = async (name: string) => { return browserProvider.exists(workspacePath) } -export const initWorkspace = (filePanelPlugin) => async (dispatch: React.Dispatch) => { - plugin = filePanelPlugin - const queryParams = new QueryParams() - // const gistHandler = new GistHandler() - const params = queryParams.get() - // let loadedFromGist = false - const workspaces = await getWorkspaces() || [] - - dispatch(setWorkspaces(workspaces)) - // if (params.gist) { - // initialWorkspace = 'gist-sample' - // await filePanelPlugin.fileProviders.workspace.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('code-sample', true, 'code-template') - dispatch(setCurrentWorkspace('code-sample')) - } else { - if (workspaces.length === 0) { - await createWorkspaceTemplate('default_workspace') - dispatch(setCurrentWorkspace('default_workspace')) - } else { - if (workspaces.length > 0) { - plugin.fileProviders.workspace.setWorkspace(workspaces[workspaces.length - 1]) - dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1])) - } - } - } - - // plugin.fileProviders.localhost.event.off('disconnected', localhostDisconnect) - // plugin.fileProviders.localhost.event.on('disconnected', localhostDisconnect) - // plugin.fileProviders.localhost.event.on('connected', () => { - // remixdExplorer.show() - // setWorkspace(LOCALHOST) - // }) - - // plugin.fileProviders.localhost.event.on('disconnected', () => { - // remixdExplorer.hide() - // }) - - // plugin.fileProviders.localhost.event.on('loading', () => { - // remixdExplorer.loading() - // }) - - // plugin.fileProviders.workspace.event.on('createWorkspace', (name) => { - // createNewWorkspace(name) - // }) -} - const getWorkspaces = async (): Promise | undefined => { try { const workspaces: string[] = await new Promise((resolve, reject) => { @@ -152,3 +147,201 @@ const getWorkspaces = async (): Promise | undefined => { console.log(e) } } + +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('code-sample', true, 'code-template') + dispatch(setCurrentWorkspace('code-sample')) + } else { + if (workspaces.length === 0) { + await createWorkspaceTemplate('default_workspace') + dispatch(setCurrentWorkspace('default_workspace')) + } else { + if (workspaces.length > 0) { + provider.setWorkspace(workspaces[workspaces.length - 1]) + dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1])) + } + } + } + // provider.event.on('fileAdded', async (filePath) => { + // await executeEvent('fileAdded', filePath) + // }) + // provider.event.on('folderAdded', async (folderPath) => { + // await executeEvent('folderAdded', folderPath) + // }) + // provider.event.on('fileRemoved', async (removePath) => { + // await executeEvent('fileRemoved', removePath) + // }) + // provider.event.on('fileRenamed', async (oldPath) => { + // await executeEvent('fileRenamed', oldPath) + // }) + // provider.event.on('rootFolderChanged', async () => { + // await executeEvent('rootFolderChanged') + // }) + // provider.event.on('fileExternallyChanged', async (path: string, file: { content: string }) => { + // const config = plugin.registry.get('config').api + // const editor = plugin.registry.get('editor').api + + // if (config.get('currentFile') === path && editor.currentContent() !== file.content) { + // if (provider.isReadOnly(path)) return editor.setText(file.content) + // dispatch(displayNotification( + // path + ' changed', + // 'This file has been changed outside of Remix IDE.', + // 'Replace by the new content', 'Keep the content displayed in Remix', + // () => { + // editor.setText(file.content) + // } + // )) + // } + // }) + // provider.event.on('fileRenamedError', async () => { + // dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel')) + // }) + // dispatch(fetchProviderSuccess(provider)) + + // provider.event.on('createWorkspace', (name) => { + // createNewWorkspace(name) + // }) + dispatch(setWorkspaces(workspaces)) + dispatch(setMode('browser')) + } +} + +export const initLocalhost = (filePanelPlugin) => async (dispatch: React.Dispatch) => { + if (filePanelPlugin) { + // plugin.fileProviders.localhost.event.off('disconnected', localhostDisconnect) + // plugin.fileProviders.localhost.event.on('disconnected', localhostDisconnect) + // plugin.fileProviders.localhost.event.on('connected', () => { + // remixdExplorer.show() + // setWorkspace(LOCALHOST) + // }) + + // plugin.fileProviders.localhost.event.on('disconnected', () => { + // remixdExplorer.hide() + // }) + + // plugin.fileProviders.localhost.event.on('loading', () => { + // remixdExplorer.loading() + // }) + dispatch(setMode('localhost')) + } +} + +// const fileAdded = async (filePath: string) => { +// if (extractParentFromKey(filePath) === '/.workspaces') return +// const path = extractParentFromKey(filePath) || provider.workspace || provider.type || '' +// const data = await fetchDirectoryContent(provider, path) + +// await dispatch(fileAddedSuccess(path, data)) +// if (filePath.includes('_test.sol')) { +// plugin.emit('newTestFileCreated', filePath) +// } +// } + +// const folderAdded = async (folderPath: string) => { +// if (extractParentFromKey(folderPath) === '/.workspaces') return +// const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || '' +// const data = await fetchDirectoryContent(provider, path) + +// await dispatch(folderAddedSuccess(path, data)) +// } + +// const fileRemoved = async (removePath: string) => { +// const path = extractParentFromKey(removePath) || provider.workspace || provider.type || '' + +// await dispatch(fileRemovedSuccess(path, removePath)) +// } + +// const fileRenamed = async (oldPath: string) => { +// const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || '' +// const data = await fetchDirectoryContent(provider, path) + +// await dispatch(fileRenamedSuccess(path, oldPath, data)) +// } + +// const rootFolderChanged = async () => { +// const workspaceName = provider.workspace || provider.type || '' + +// await fetchDirectory(provider, workspaceName)(dispatch) +// } + +// const executeEvent = async (eventName: 'fileAdded' | 'folderAdded' | 'fileRemoved' | 'fileRenamed' | 'rootFolderChanged', path?: string) => { +// if (Object.keys(pendingEvents).length) { +// return queuedEvents.push({ eventName, path }) +// } +// pendingEvents[eventName + path] = { eventName, path } +// switch (eventName) { +// case 'fileAdded': +// await fileAdded(path) +// delete pendingEvents[eventName + path] +// if (queuedEvents.length) { +// const next = queuedEvents.pop() + +// await executeEvent(next.eventName, next.path) +// } +// break + +// case 'folderAdded': +// await folderAdded(path) +// delete pendingEvents[eventName + path] +// if (queuedEvents.length) { +// const next = queuedEvents.pop() + +// await executeEvent(next.eventName, next.path) +// } +// break + +// case 'fileRemoved': +// await fileRemoved(path) +// delete pendingEvents[eventName + path] +// if (queuedEvents.length) { +// const next = queuedEvents.pop() + +// await executeEvent(next.eventName, next.path) +// } +// break + +// case 'fileRenamed': +// await fileRenamed(path) +// delete pendingEvents[eventName + path] +// if (queuedEvents.length) { +// const next = queuedEvents.pop() + +// await executeEvent(next.eventName, next.path) +// } +// break + +// case 'rootFolderChanged': +// await rootFolderChanged() +// delete pendingEvents[eventName + path] +// if (queuedEvents.length) { +// const next = queuedEvents.pop() + +// await executeEvent(next.eventName, next.path) +// } +// break +// } +// } diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts new file mode 100644 index 0000000000..1108fcb0e0 --- /dev/null +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react' +import { BrowserState } from '../reducers/workspace' + +export const FileSystemContext = createContext<{ + fs: BrowserState, + modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, + dispatchInitWorkspace:() => void, + dispatchInitLocalhost:() => void, + dispatchFetchDirectory:(path: string) => void + }>(null) diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx new file mode 100644 index 0000000000..40f1c18376 --- /dev/null +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -0,0 +1,83 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React, { useReducer, useState, useEffect } from 'react' +// 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' + +export const FileSystemProvider = ({ filePanel, children }) => { + const [fs, fsDispatch] = useReducer(browserReducer, browserInitialState) + const [focusModal, setFocusModal] = useState({ + hide: true, + title: '', + message: '', + okLabel: '', + okFn: () => {}, + cancelLabel: '', + cancelFn: () => {} + }) + const [modals, setModals] = useState([]) + + const dispatchInitWorkspace = async () => { + await initWorkspace(filePanel)(fsDispatch) + } + + const dispatchInitLocalhost = async () => { + await initLocalhost(filePanel)(fsDispatch) + } + + const dispatchFetchDirectory = async (path: string) => { + await fetchDirectory(fs.mode, path)(fsDispatch) + } + + useEffect(() => { + if (modals.length > 0) { + setModals(modals => { + const focusModal = { + hide: false, + title: modals[0].title, + message: modals[0].message, + okLabel: modals[0].okLabel, + okFn: modals[0].okFn, + cancelLabel: modals[0].cancelLabel, + cancelFn: modals[0].cancelFn + } + + modals.shift() + return { + ...modals, + focusModal, + modals: modals + } + }) + } + }, [modals]) + + const handleHideModal = () => { + setFocusModal(modal => { + return { ...modal, hide: true, message: null } + }) + } + + 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 }]) + } + + const value = { + fs, + modal, + dispatchInitWorkspace, + dispatchInitLocalhost, + dispatchFetchDirectory + } + return ( + + { children } + + + ) +} + +export default FileSystemProvider diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index 403bde8396..a4183f4254 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -1,46 +1,111 @@ +import { extractNameFromKey } from '@remix-ui/file-explorer' interface Action { type: string - payload: Record | string + payload: any } -interface State { +export interface BrowserState { browser: { currentWorkspace: string, workspaces: string[], + files: [] isRequesting: boolean, isSuccessful: boolean, error: string - } + }, + localhost: { + files: [], + isRequesting: boolean, + isSuccessful: boolean, + error: string + }, + mode: 'browser' | 'localhost' } -export const browserInitialState: State = { +export const browserInitialState: BrowserState = { browser: { currentWorkspace: '', workspaces: [], + files: [], isRequesting: false, isSuccessful: false, error: null - } + }, + localhost: { + files: [], + isRequesting: false, + isSuccessful: false, + error: null + }, + mode: 'browser' } export const browserReducer = (state = browserInitialState, action: Action) => { switch (action.type) { case 'SET_CURRENT_WORKSPACE': { + const payload = action.payload as string + return { ...state, browser: { ...state.browser, - currentWorkspace: typeof action.payload === 'string' ? action.payload : '', - workspaces: typeof action.payload === 'string' ? state.browser.workspaces.includes(action.payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] : state.browser.workspaces + currentWorkspace: payload, + workspaces: state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] } } } case 'SET_WORKSPACES': { + const payload = action.payload as string[] + + return { + ...state, + browser: { + ...state.browser, + workspaces: payload + } + } + } + + case 'SET_MODE': { + return { + ...state, + mode: action.payload + } + } + + case 'FETCH_DIRECTORY_REQUEST': { + return { + ...state, + browser: { + ...state.browser, + isRequesting: true, + isSuccessful: false, + error: null + } + } + } + case 'FETCH_DIRECTORY_SUCCESS': { + const payload = action.payload as { path: string, files } + return { ...state, browser: { ...state.browser, - workspaces: Array.isArray(action.payload) ? action.payload : state.browser.workspaces + files: fetchDirectoryContent(payload.files, payload.path), + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'FETCH_DIRECTORY_ERROR': { + return { + ...state, + browser: { + ...state.browser, + isRequesting: false, + isSuccessful: false, + error: action.payload } } } @@ -48,3 +113,58 @@ export const browserReducer = (state = browserInitialState, action: Action) => { throw new Error() } } + +const fetchDirectoryContent = (fileTree, folderPath: string) => { + const files = normalize(fileTree) + + return { [extractNameFromKey(folderPath)]: files } +} + +const normalize = (filesList): any => { + const folders = {} + const files = {} + + Object.keys(filesList || {}).forEach(key => { + key = key.replace(/^\/|\/$/g, '') // remove first and last slash + let path = key + path = path.replace(/^\/|\/$/g, '') // remove first and last slash + + if (filesList[key].isDirectory) { + folders[extractNameFromKey(key)] = { + path, + name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path), + isDirectory: filesList[key].isDirectory, + type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder' + } + } else { + files[extractNameFromKey(key)] = { + path, + name: extractNameFromKey(path), + isDirectory: filesList[key].isDirectory, + type: 'file' + } + } + }) + + // if (newInputType === 'folder') { + // const path = parent + '/blank' + + // folders[path] = { + // path: path, + // name: '', + // isDirectory: true, + // type: 'folder' + // } + // } else if (newInputType === 'file') { + // const path = parent + '/blank' + + // files[path] = { + // path: path, + // name: '', + // isDirectory: false, + // type: 'file' + // } + // } + + return Object.assign({}, folders, 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 48dd7c37a0..4f4f26493d 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -1,11 +1,9 @@ -import React, { useState, useEffect, useRef, useReducer } from 'react' // eslint-disable-line -import { FileExplorer, MenuItems } from '@remix-ui/file-explorer' // eslint-disable-line +import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line +import { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line import './remix-ui-workspace.css' -import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line import { Toaster } from '@remix-ui/toaster' // eslint-disable-line -import { WorkspaceProps, WorkspaceState, Modal } from './types' -import { initWorkspace } from './actions/workspace' -import { browserReducer, browserInitialState } from './reducers/workspace' +import { WorkspaceProps, WorkspaceState } from './types' +import { FileSystemContext } from './contexts' const canUpload = window.File || window.FileReader || window.FileList || window.Blob @@ -22,26 +20,16 @@ export function Workspace (props: WorkspaceProps) { loadingLocalhost: false, toasterMsg: '' }) - const [modal, setModal] = useState({ - hide: true, - title: '', - message: null, - okLabel: '', - okFn: () => {}, - cancelLabel: '', - cancelFn: () => {}, - handleHide: null - }) const [currentWorkspace, setCurrentWorkspace] = useState(NO_WORKSPACE) - const [fs, dispatch] = useReducer(browserReducer, browserInitialState) + const global = useContext(FileSystemContext) useEffect(() => { - initWorkspace(props.plugin)(dispatch) + global.dispatchInitWorkspace() }, []) useEffect(() => { - if (fs.browser.currentWorkspace) setCurrentWorkspace(fs.browser.currentWorkspace) - }, [fs.browser.currentWorkspace]) + if (global.fs.browser.currentWorkspace) setCurrentWorkspace(global.fs.browser.currentWorkspace) + }, [global.fs.browser.currentWorkspace]) props.plugin.resetNewFile = () => { setState(prevState => { @@ -91,7 +79,7 @@ export function Workspace (props: WorkspaceProps) { await setWorkspace(workspaceName) toast('New default workspace has been created.') } catch (e) { - modalMessage('Create Default Workspace', e.message) + global.modal('Create Default Workspace', e.message, 'OK', onFinishRenameWorkspace, '') console.error(e) } } @@ -105,21 +93,15 @@ export function Workspace (props: WorkspaceProps) { /* workspace creation, renaming and deletion */ const renameCurrentWorkspace = () => { - modalDialog('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '', () => {}) + global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '') } const createWorkspace = () => { - modalDialog('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '', () => {}) + global.modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '') } const deleteCurrentWorkspace = () => { - modalDialog('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '', () => {}) - } - - const modalMessage = (title: string, body: string) => { - setTimeout(() => { // wait for any previous modal a chance to close - modalDialog(title, body, 'OK', () => {}, '', null) - }, 200) + global.modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '') } const workspaceRenameInput = useRef() @@ -135,7 +117,7 @@ export function Workspace (props: WorkspaceProps) { setWorkspace(workspaceName) props.plugin.workspaceRenamed({ name: workspaceName }) } catch (e) { - modalMessage('Rename Workspace', e.message) + global.modal('Rename Workspace', e.message, 'OK', () => {}, '') console.error(e) } } @@ -150,7 +132,7 @@ export function Workspace (props: WorkspaceProps) { await props.plugin.createWorkspace(workspaceName) await setWorkspace(workspaceName) } catch (e) { - modalMessage('Create Workspace', e.message) + global.modal('Create Workspace', e.message, 'OK', () => {}, '') console.error(e) } } @@ -216,22 +198,9 @@ export function Workspace (props: WorkspaceProps) { } } - const handleHideModal = () => { - setModal(prevModal => { - return { ...prevModal, hide: true, message: null } - }) - } - - const modalDialog = async (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel: string, cancelFn: () => void) => { - await setModal(prevModal => { - return { ...prevModal, hide: false, message, title, okLabel, okFn, cancelLabel, cancelFn, handleHide: handleHideModal } - }) - } - const createModalMessage = () => { return ( <> - { modal.message } ) @@ -240,7 +209,6 @@ export function Workspace (props: WorkspaceProps) { const renameModalMessage = () => { return ( <> - { modal.message } ) @@ -248,10 +216,6 @@ export function Workspace (props: WorkspaceProps) { return (
- - { (typeof modal.message !== 'string') && modal.message } - - }
resetFocus(true)}>
@@ -296,13 +260,13 @@ export function Workspace (props: WorkspaceProps) {
diff --git a/libs/remix-ui/workspace/src/lib/types/index.ts b/libs/remix-ui/workspace/src/lib/types/index.ts index 8b1364e406..caca5a1856 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -40,12 +40,11 @@ export interface WorkspaceState { } export interface Modal { - hide: boolean + hide?: boolean title: string message: string | JSX.Element okLabel: string okFn: () => void cancelLabel: string cancelFn: () => void - handleHide: () => void } diff --git a/nx.json b/nx.json index f7d1a98e4d..a5cae4682b 100644 --- a/nx.json +++ b/nx.json @@ -132,6 +132,9 @@ }, "remix-ui-editor": { "tags": [] + }, + "remix-ui-helper": { + "tags": [] } }, "targetDependencies": { diff --git a/tsconfig.base.json b/tsconfig.base.json index 8339f5d9e5..07d0225fe0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -47,10 +47,11 @@ "@remix-project/core-plugin": ["libs/remix-core-plugin/src/index.ts"], "@remix-ui/solidity-compiler": ["libs/remix-ui/solidity-compiler/src/index.ts"], "@remix-ui/publish-to-storage": ["libs/remix-ui/publish-to-storage/src/index.ts"], + "@remix-ui/plugin-manager": ["libs/remix-ui/plugin-manager/src/index.ts"], "@remix-ui/renderer": ["libs/remix-ui/renderer/src/index.ts"], "@remix-ui/terminal": ["libs/remix-ui/terminal/src/index.ts"], - "@remix-ui/plugin-manager": ["libs/remix-ui/plugin-manager/src/index.ts"], - "@remix-ui/editor": ["libs/remix-ui/editor/src/index.ts"] + "@remix-ui/editor": ["libs/remix-ui/editor/src/index.ts"], + "@remix-ui/helper": ["libs/remix-ui/helper/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 4fbb8eb54c..890d74e2fc 100644 --- a/workspace.json +++ b/workspace.json @@ -1051,6 +1051,22 @@ } } } + }, + "remix-ui-helper": { + "root": "libs/remix-ui/helper", + "sourceRoot": "libs/remix-ui/helper/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "tsConfig": ["libs/remix-ui/helper/tsconfig.lib.json"], + "exclude": ["**/node_modules/**", "!libs/remix-ui/helper/**/*"] + } + } + } } }, "cli": {