Fetch directory from global FS

pull/1575/head
ioedeveloper 3 years ago
parent 14cc1c6ee3
commit da9207ddd7
  1. 6
      apps/remix-ide/src/app/panels/file-panel.js
  2. 1
      libs/remix-ui/file-explorer/src/index.ts
  3. 120
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  4. 1
      libs/remix-ui/helper/.eslintrc
  5. 3
      libs/remix-ui/helper/README.md
  6. 1
      libs/remix-ui/helper/src/index.ts
  7. 3
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  8. 10
      libs/remix-ui/helper/tsconfig.json
  9. 12
      libs/remix-ui/helper/tsconfig.lib.json
  10. 2
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  11. 1
      libs/remix-ui/workspace/src/index.ts
  12. 313
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  13. 10
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  14. 83
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  15. 136
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  16. 68
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  17. 3
      libs/remix-ui/workspace/src/lib/types/index.ts
  18. 3
      nx.json
  19. 5
      tsconfig.base.json
  20. 16
      workspace.json

@ -3,7 +3,7 @@ import { ViewPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom' 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' import { checkSpecialChars, checkSlash } from '../../lib/helper'
const { RemixdHandle } = require('../files/remixd-handle.js') const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js') const { GitHandle } = require('../files/git-handle.js')
@ -69,7 +69,9 @@ module.exports = class Filepanel extends ViewPlugin {
renderComponent () { renderComponent () {
ReactDOM.render( ReactDOM.render(
<Workspace plugin={this} /> <FileSystemProvider plugin={this}>
<Workspace plugin={this} />
</FileSystemProvider>
, this.el) , this.el)
} }

@ -1,2 +1,3 @@
export * from './lib/file-explorer' export * from './lib/file-explorer'
export * from './lib/types' export * from './lib/types'
export * from './lib/utils'

@ -1,7 +1,6 @@
import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint-disable-line import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint-disable-line
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // 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 { 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 { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import Gists from 'gists' import Gists from 'gists'
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
@ -19,12 +18,25 @@ const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems, resetFocus } = props 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: [{ focusElement: [{
key: '', key: '',
type: 'folder' type: 'folder'
}], }],
files: [],
fileManager: null, fileManager: null,
ctrlKey: false, ctrlKey: false,
newFileName: '', newFileName: '',
@ -32,90 +44,60 @@ export const FileExplorer = (props: FileExplorerProps) => {
id: 'newFile', id: 'newFile',
name: 'New File', name: 'New File',
type: ['folder', 'gist'], type: ['folder', 'gist'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'newFolder', id: 'newFolder',
name: 'New Folder', name: 'New Folder',
type: ['folder', 'gist'], type: ['folder', 'gist'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'rename', id: 'rename',
name: 'Rename', name: 'Rename',
type: ['file', 'folder'], type: ['file', 'folder'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'delete', id: 'delete',
name: 'Delete', name: 'Delete',
type: ['file', 'folder', 'gist'], type: ['file', 'folder', 'gist'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'run', id: 'run',
name: 'Run', name: 'Run',
type: [],
path: [],
extension: ['.js'], extension: ['.js'],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'pushChangesToGist', id: 'pushChangesToGist',
name: 'Push changes to gist', name: 'Push changes to gist',
type: ['gist'], type: ['gist'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'publishFolderToGist', id: 'publishFolderToGist',
name: 'Publish folder to gist', name: 'Publish folder to gist',
type: ['folder'], type: ['folder'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'publishFileToGist', id: 'publishFileToGist',
name: 'Publish file to gist', name: 'Publish file to gist',
type: ['file'], type: ['file'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'copy', id: 'copy',
name: 'Copy', name: 'Copy',
type: ['folder', 'file'], type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'deleteAll', id: 'deleteAll',
name: 'Delete All', name: 'Delete All',
type: ['folder', 'file'], type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: true, multiselect: true,
label: '' label: ''
}], }],
@ -132,17 +114,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
lastEdit: '' lastEdit: ''
}, },
expandPath: [name], expandPath: [name],
focusModal: {
hide: true,
title: '',
message: '',
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {},
handleHide: null
},
modals: [],
toasterMsg: '', toasterMsg: '',
mouseOverElement: null, mouseOverElement: null,
showContextMenu: false, showContextMenu: false,
@ -233,30 +204,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, [externalUploads]) }, [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(() => { useEffect(() => {
const keyPressHandler = (e: KeyboardEvent) => { const keyPressHandler = (e: KeyboardEvent) => {
if (e.shiftKey) { if (e.shiftKey) {
@ -630,30 +577,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
plugin.call(cmd.id, cmd.name, cmd) 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) => { const toast = (message: string) => {
setState(prevState => { setState(prevState => {
return { ...prevState, toasterMsg: message } return { ...prevState, toasterMsg: message }
@ -1012,19 +935,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
</div> </div>
</TreeViewItem> </TreeViewItem>
</TreeView> </TreeView>
{
props.name && <ModalDialog
id={ props.name }
title={ state.focusModal.title }
message={ state.focusModal.message }
hide={ state.focusModal.hide }
okLabel={ state.focusModal.okLabel }
okFn={ state.focusModal.okFn }
cancelLabel={ state.focusModal.cancelLabel }
cancelFn={ state.focusModal.cancelFn }
handleHide={ handleHideModal }
/>
}
<Toaster message={state.toasterMsg} /> <Toaster message={state.toasterMsg} />
{ state.showContextMenu && { state.showContextMenu &&
<FileExplorerContextMenu <FileExplorerContextMenu

@ -0,0 +1 @@
{ "extends": "../../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] }

@ -0,0 +1,3 @@
# remix-ui-helper
This library was generated with [Nx](https://nx.dev).

@ -0,0 +1 @@
export * from './lib/remix-ui-helper';

@ -0,0 +1,3 @@
export function remixUiHelper(): string {
return 'remix-ui-helper';
}

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../dist/out-tsc",
"declaration": true,
"rootDir": "./src",
"types": ["node"]
},
"exclude": ["**/*.spec.ts"],
"include": ["**/*.ts"]
}

@ -9,7 +9,7 @@ export interface ModalDialogProps {
cancelFn?: () => void, cancelFn?: () => void,
modalClass?: string, modalClass?: string,
showCancelIcon?: boolean, showCancelIcon?: boolean,
hide: boolean, hide?: boolean,
handleHide: (hideState?: boolean) => void, handleHide: (hideState?: boolean) => void,
children?: React.ReactNode children?: React.ReactNode
} }

@ -1 +1,2 @@
export * from './lib/remix-ui-workspace' export * from './lib/remix-ui-workspace'
export * from './lib/providers/FileSystemProvider'

@ -1,10 +1,13 @@
import { bufferToHex, keccakFromString } from 'ethereumjs-util' import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import { checkSpecialChars, checkSlash } from '../../../../../../apps/remix-ide/src/lib/helper' 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 GistHandler = require('../../../../../../apps/remix-ide/src/lib/gist-handler')
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params')
const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples') const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples')
let plugin // const queuedEvents = []
// const pendingEvents = {}
let plugin, dispatch: React.Dispatch<any>
const setCurrentWorkspace = (workspace: string) => { const setCurrentWorkspace = (workspace: string) => {
return { 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<any>) => {
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<any>) => {
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') => { 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 (!workspaceName) throw new Error('workspace name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') 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 { try {
const queryParams = new QueryParams() const queryParams = new QueryParams()
const params = queryParams.get() const params = queryParams.get()
await plugin.fileProviders.worspace.createWorkspace(workspaceName)
await workspaceProvider.createWorkspace(workspaceName)
const hash = bufferToHex(keccakFromString(params.code)) const hash = bufferToHex(keccakFromString(params.code))
const fileName = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol' const fileName = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol'
const path = fileName const path = fileName
await plugin.fileProviders.workspace.set(path, atob(params.code)) await workspaceProvider.set(path, atob(params.code))
await plugin.fileManager.openFile(fileName) await plugin.fileManager.openFile(fileName)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -73,63 +125,6 @@ const workspaceExists = async (name: string) => {
return browserProvider.exists(workspacePath) return browserProvider.exists(workspacePath)
} }
export const initWorkspace = (filePanelPlugin) => async (dispatch: React.Dispatch<any>) => {
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<string[]> | undefined => { const getWorkspaces = async (): Promise<string[]> | undefined => {
try { try {
const workspaces: string[] = await new Promise((resolve, reject) => { const workspaces: string[] = await new Promise((resolve, reject) => {
@ -152,3 +147,201 @@ const getWorkspaces = async (): Promise<string[]> | undefined => {
console.log(e) console.log(e)
} }
} }
export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch<any>) => {
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<any>) => {
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
// }
// }

@ -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)

@ -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<Modal>({
hide: true,
title: '',
message: '',
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {}
})
const [modals, setModals] = useState<Modal[]>([])
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 (
<FileSystemContext.Provider value={value}>
{ children }
<ModalDialog id='fileSystem' { ...focusModal } handleHide={ handleHideModal } />
</FileSystemContext.Provider>
)
}
export default FileSystemProvider

@ -1,46 +1,111 @@
import { extractNameFromKey } from '@remix-ui/file-explorer'
interface Action { interface Action {
type: string type: string
payload: Record<string, any> | string payload: any
} }
interface State { export interface BrowserState {
browser: { browser: {
currentWorkspace: string, currentWorkspace: string,
workspaces: string[], workspaces: string[],
files: []
isRequesting: boolean, isRequesting: boolean,
isSuccessful: boolean, isSuccessful: boolean,
error: string error: string
} },
localhost: {
files: [],
isRequesting: boolean,
isSuccessful: boolean,
error: string
},
mode: 'browser' | 'localhost'
} }
export const browserInitialState: State = { export const browserInitialState: BrowserState = {
browser: { browser: {
currentWorkspace: '', currentWorkspace: '',
workspaces: [], workspaces: [],
files: [],
isRequesting: false, isRequesting: false,
isSuccessful: false, isSuccessful: false,
error: null error: null
} },
localhost: {
files: [],
isRequesting: false,
isSuccessful: false,
error: null
},
mode: 'browser'
} }
export const browserReducer = (state = browserInitialState, action: Action) => { export const browserReducer = (state = browserInitialState, action: Action) => {
switch (action.type) { switch (action.type) {
case 'SET_CURRENT_WORKSPACE': { case 'SET_CURRENT_WORKSPACE': {
const payload = action.payload as string
return { return {
...state, ...state,
browser: { browser: {
...state.browser, ...state.browser,
currentWorkspace: typeof action.payload === 'string' ? action.payload : '', currentWorkspace: payload,
workspaces: typeof action.payload === 'string' ? state.browser.workspaces.includes(action.payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] : state.browser.workspaces workspaces: state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
} }
} }
} }
case 'SET_WORKSPACES': { 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 { return {
...state, ...state,
browser: { browser: {
...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() 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)
}

@ -1,11 +1,9 @@
import React, { useState, useEffect, useRef, useReducer } from 'react' // eslint-disable-line import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line
import { FileExplorer, MenuItems } from '@remix-ui/file-explorer' // eslint-disable-line import { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line
import './remix-ui-workspace.css' 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 { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import { WorkspaceProps, WorkspaceState, Modal } from './types' import { WorkspaceProps, WorkspaceState } from './types'
import { initWorkspace } from './actions/workspace' import { FileSystemContext } from './contexts'
import { browserReducer, browserInitialState } from './reducers/workspace'
const canUpload = window.File || window.FileReader || window.FileList || window.Blob const canUpload = window.File || window.FileReader || window.FileList || window.Blob
@ -22,26 +20,16 @@ export function Workspace (props: WorkspaceProps) {
loadingLocalhost: false, loadingLocalhost: false,
toasterMsg: '' toasterMsg: ''
}) })
const [modal, setModal] = useState<Modal>({
hide: true,
title: '',
message: null,
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {},
handleHide: null
})
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE) const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const [fs, dispatch] = useReducer(browserReducer, browserInitialState) const global = useContext(FileSystemContext)
useEffect(() => { useEffect(() => {
initWorkspace(props.plugin)(dispatch) global.dispatchInitWorkspace()
}, []) }, [])
useEffect(() => { useEffect(() => {
if (fs.browser.currentWorkspace) setCurrentWorkspace(fs.browser.currentWorkspace) if (global.fs.browser.currentWorkspace) setCurrentWorkspace(global.fs.browser.currentWorkspace)
}, [fs.browser.currentWorkspace]) }, [global.fs.browser.currentWorkspace])
props.plugin.resetNewFile = () => { props.plugin.resetNewFile = () => {
setState(prevState => { setState(prevState => {
@ -91,7 +79,7 @@ export function Workspace (props: WorkspaceProps) {
await setWorkspace(workspaceName) await setWorkspace(workspaceName)
toast('New default workspace has been created.') toast('New default workspace has been created.')
} catch (e) { } catch (e) {
modalMessage('Create Default Workspace', e.message) global.modal('Create Default Workspace', e.message, 'OK', onFinishRenameWorkspace, '')
console.error(e) console.error(e)
} }
} }
@ -105,21 +93,15 @@ export function Workspace (props: WorkspaceProps) {
/* workspace creation, renaming and deletion */ /* workspace creation, renaming and deletion */
const renameCurrentWorkspace = () => { const renameCurrentWorkspace = () => {
modalDialog('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '', () => {}) global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '')
} }
const createWorkspace = () => { const createWorkspace = () => {
modalDialog('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '', () => {}) global.modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '')
} }
const deleteCurrentWorkspace = () => { const deleteCurrentWorkspace = () => {
modalDialog('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '', () => {}) global.modal('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)
} }
const workspaceRenameInput = useRef() const workspaceRenameInput = useRef()
@ -135,7 +117,7 @@ export function Workspace (props: WorkspaceProps) {
setWorkspace(workspaceName) setWorkspace(workspaceName)
props.plugin.workspaceRenamed({ name: workspaceName }) props.plugin.workspaceRenamed({ name: workspaceName })
} catch (e) { } catch (e) {
modalMessage('Rename Workspace', e.message) global.modal('Rename Workspace', e.message, 'OK', () => {}, '')
console.error(e) console.error(e)
} }
} }
@ -150,7 +132,7 @@ export function Workspace (props: WorkspaceProps) {
await props.plugin.createWorkspace(workspaceName) await props.plugin.createWorkspace(workspaceName)
await setWorkspace(workspaceName) await setWorkspace(workspaceName)
} catch (e) { } catch (e) {
modalMessage('Create Workspace', e.message) global.modal('Create Workspace', e.message, 'OK', () => {}, '')
console.error(e) 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 = () => { const createModalMessage = () => {
return ( return (
<> <>
<span>{ modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" /> <input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" />
</> </>
) )
@ -240,7 +209,6 @@ export function Workspace (props: WorkspaceProps) {
const renameModalMessage = () => { const renameModalMessage = () => {
return ( return (
<> <>
<span>{ modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ currentWorkspace } ref={workspaceRenameInput} className="form-control" /> <input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ currentWorkspace } ref={workspaceRenameInput} className="form-control" />
</> </>
) )
@ -248,10 +216,6 @@ export function Workspace (props: WorkspaceProps) {
return ( return (
<div className='remixui_container'> <div className='remixui_container'>
<ModalDialog id='workspacesModalDialog' {...modal}>
{ (typeof modal.message !== 'string') && modal.message }
</ModalDialog>
}
<Toaster message={state.toasterMsg} /> <Toaster message={state.toasterMsg} />
<div className='remixui_fileexplorer' onClick={() => resetFocus(true)}> <div className='remixui_fileexplorer' onClick={() => resetFocus(true)}>
<div> <div>
@ -296,13 +260,13 @@ export function Workspace (props: WorkspaceProps) {
</span> </span>
<select id="workspacesSelect" value={currentWorkspace} data-id="workspacesSelect" onChange={(e) => setWorkspace(e.target.value)} className="form-control custom-select"> <select id="workspacesSelect" value={currentWorkspace} data-id="workspacesSelect" onChange={(e) => setWorkspace(e.target.value)} className="form-control custom-select">
{ {
fs.browser.workspaces global.fs.browser.workspaces
.map((folder, index) => { .map((folder, index) => {
return <option key={index} value={folder}>{folder}</option> return <option key={index} value={folder}>{folder}</option>
}) })
} }
<option value={LOCALHOST}>{currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option> <option value={LOCALHOST}>{currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option>
{ fs.browser.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> } { global.fs.browser.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
</select> </select>
</div> </div>
</header> </header>

@ -40,12 +40,11 @@ export interface WorkspaceState {
} }
export interface Modal { export interface Modal {
hide: boolean hide?: boolean
title: string title: string
message: string | JSX.Element message: string | JSX.Element
okLabel: string okLabel: string
okFn: () => void okFn: () => void
cancelLabel: string cancelLabel: string
cancelFn: () => void cancelFn: () => void
handleHide: () => void
} }

@ -132,6 +132,9 @@
}, },
"remix-ui-editor": { "remix-ui-editor": {
"tags": [] "tags": []
},
"remix-ui-helper": {
"tags": []
} }
}, },
"targetDependencies": { "targetDependencies": {

@ -47,10 +47,11 @@
"@remix-project/core-plugin": ["libs/remix-core-plugin/src/index.ts"], "@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/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/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/renderer": ["libs/remix-ui/renderer/src/index.ts"],
"@remix-ui/terminal": ["libs/remix-ui/terminal/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"] "exclude": ["node_modules", "tmp"]

@ -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": { "cli": {

Loading…
Cancel
Save