Fetch directory from global FS

pull/5370/head
ioedeveloper 3 years ago
parent 3624a73e3a
commit 20d9f54dc9
  1. 4
      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. 257
      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 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(
<FileSystemProvider plugin={this}>
<Workspace plugin={this} />
</FileSystemProvider>
, this.el)
}

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

@ -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) => {
</div>
</TreeViewItem>
</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} />
{ state.showContextMenu &&
<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,
modalClass?: string,
showCancelIcon?: boolean,
hide: boolean,
hide?: boolean,
handleHide: (hideState?: boolean) => void,
children?: React.ReactNode
}

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

@ -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<any>
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<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') => {
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,18 +125,43 @@ const workspaceExists = async (name: string) => {
return browserProvider.exists(workspacePath)
}
export const initWorkspace = (filePanelPlugin) => async (dispatch: React.Dispatch<any>) => {
const getWorkspaces = async (): Promise<string[]> | undefined => {
try {
const workspaces: string[] = await new Promise((resolve, reject) => {
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) {
console.error(error)
return reject(error)
}
resolve(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map((folder) => folder.replace(workspacesPath + '/', '')))
})
})
return workspaces
} catch (e) {
// modalDialogCustom.alert('Workspaces have not been created on your system. Please use "Migrate old filesystem to workspace" on the home page to transfer your files or start by creating a new workspace in the File Explorers.')
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() || []
dispatch(setWorkspaces(workspaces))
// if (params.gist) {
// initialWorkspace = 'gist-sample'
// await filePanelPlugin.fileProviders.workspace.createWorkspace(initialWorkspace)
// await provider.createWorkspace(initialWorkspace)
// loadedFromGist = gistHandler.loadFromGist(params, plugin.fileManager)
// }
// if (loadedFromGist) {
@ -104,12 +181,57 @@ export const initWorkspace = (filePanelPlugin) => async (dispatch: React.Dispatc
dispatch(setCurrentWorkspace('default_workspace'))
} else {
if (workspaces.length > 0) {
plugin.fileProviders.workspace.setWorkspace(workspaces[workspaces.length - 1])
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', () => {
@ -124,31 +246,102 @@ export const initWorkspace = (filePanelPlugin) => async (dispatch: React.Dispatc
// plugin.fileProviders.localhost.event.on('loading', () => {
// remixdExplorer.loading()
// })
// plugin.fileProviders.workspace.event.on('createWorkspace', (name) => {
// createNewWorkspace(name)
// })
dispatch(setMode('localhost'))
}
}
const getWorkspaces = async (): Promise<string[]> | undefined => {
try {
const workspaces: string[] = await new Promise((resolve, reject) => {
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
// const fileAdded = async (filePath: string) => {
// if (extractParentFromKey(filePath) === '/.workspaces') return
// const path = extractParentFromKey(filePath) || provider.workspace || provider.type || ''
// const data = await fetchDirectoryContent(provider, path)
plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) {
console.error(error)
return reject(error)
}
resolve(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map((folder) => folder.replace(workspacesPath + '/', '')))
})
})
// await dispatch(fileAddedSuccess(path, data))
// if (filePath.includes('_test.sol')) {
// plugin.emit('newTestFileCreated', filePath)
// }
// }
return workspaces
} catch (e) {
// modalDialogCustom.alert('Workspaces have not been created on your system. Please use "Migrate old filesystem to workspace" on the home page to transfer your files or start by creating a new workspace in the File Explorers.')
console.log(e)
}
}
// 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 {
type: string
payload: Record<string, any> | 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)
}

@ -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<Modal>({
hide: true,
title: '',
message: null,
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {},
handleHide: null
})
const [currentWorkspace, setCurrentWorkspace] = useState<string>(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 (
<>
<span>{ modal.message }</span>
<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 = () => {
return (
<>
<span>{ modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ currentWorkspace } ref={workspaceRenameInput} className="form-control" />
</>
)
@ -248,10 +216,6 @@ export function Workspace (props: WorkspaceProps) {
return (
<div className='remixui_container'>
<ModalDialog id='workspacesModalDialog' {...modal}>
{ (typeof modal.message !== 'string') && modal.message }
</ModalDialog>
}
<Toaster message={state.toasterMsg} />
<div className='remixui_fileexplorer' onClick={() => resetFocus(true)}>
<div>
@ -296,13 +260,13 @@ export function Workspace (props: WorkspaceProps) {
</span>
<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) => {
return <option key={index} value={folder}>{folder}</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>
</div>
</header>

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

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

@ -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"]

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

Loading…
Cancel
Save