Merge pull request #1575 from ethereum/refactor-workspace

Refactor workspace
pull/1676/head
David Disu 3 years ago committed by GitHub
commit 2bd8018100
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      apps/remix-ide-e2e/src/commands/removeFile.ts
  2. 13
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  3. 16
      apps/remix-ide-e2e/src/tests/gist.spec.ts
  4. 1
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  5. 21
      apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts
  6. 1
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  7. 30
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  8. 11
      apps/remix-ide/src/app.js
  9. 3
      apps/remix-ide/src/app/editor/editor.js
  10. 4
      apps/remix-ide/src/app/files/dgitProvider.js
  11. 4
      apps/remix-ide/src/app/files/fileManager.js
  12. 25
      apps/remix-ide/src/app/files/remixDProvider.js
  13. 16
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  14. 274
      apps/remix-ide/src/app/panels/file-panel.js
  15. 25
      apps/remix-ide/src/app/panels/tab-proxy.js
  16. 18
      apps/remix-ide/src/app/tabs/compile-tab.js
  17. 2
      libs/remix-tests/src/run.ts
  18. 4
      libs/remix-ui/file-explorer/.babelrc
  19. 19
      libs/remix-ui/file-explorer/.eslintrc
  20. 7
      libs/remix-ui/file-explorer/README.md
  21. 1
      libs/remix-ui/file-explorer/src/index.ts
  22. 384
      libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts
  23. 1105
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  24. 348
      libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts
  25. 59
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  26. 13
      libs/remix-ui/file-explorer/src/lib/utils/index.ts
  27. 13
      libs/remix-ui/file-explorer/tsconfig.lib.json
  28. 1
      libs/remix-ui/helper/.eslintrc
  29. 3
      libs/remix-ui/helper/README.md
  30. 1
      libs/remix-ui/helper/src/index.ts
  31. 63
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  32. 6
      libs/remix-ui/helper/tsconfig.json
  33. 12
      libs/remix-ui/helper/tsconfig.lib.json
  34. 2
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  35. 10
      libs/remix-ui/plugin-manager/src/lib/components/LocalPluginForm.tsx
  36. 4
      libs/remix-ui/toaster/src/lib/toaster.tsx
  37. 3
      libs/remix-ui/workspace/src/index.ts
  38. 187
      libs/remix-ui/workspace/src/lib/actions/events.ts
  39. 332
      libs/remix-ui/workspace/src/lib/actions/index.ts
  40. 234
      libs/remix-ui/workspace/src/lib/actions/payload.ts
  41. 299
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  42. 4
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  43. 2
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  44. 473
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  45. 67
      libs/remix-ui/workspace/src/lib/components/file-label.tsx
  46. 124
      libs/remix-ui/workspace/src/lib/components/file-render.tsx
  47. 32
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  48. 0
      libs/remix-ui/workspace/src/lib/css/file-explorer-context-menu.css
  49. 0
      libs/remix-ui/workspace/src/lib/css/file-explorer.css
  50. 0
      libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css
  51. 230
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  52. 815
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  53. 416
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  54. 155
      libs/remix-ui/workspace/src/lib/types/index.ts
  55. 63
      libs/remix-ui/workspace/src/lib/utils/index.ts
  56. 2
      libs/remixd/src/bin/remixd.ts
  57. 2
      libs/remixd/src/services/remixdClient.ts
  58. 6
      nx.json
  59. 7215
      package-lock.json
  60. 3
      package.json
  61. 13
      tsconfig.base.json
  62. 35
      workspace.json

@ -39,8 +39,8 @@ function removeFile (browser: NightwatchBrowser, path: string, workspace: string
.pause(2000)
.perform(() => {
console.log(path, 'to remove')
browser.waitForElementVisible('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok')
.click('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok')
browser.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('[data-path="' + path + '"]')
done()
})

@ -68,9 +68,9 @@ module.exports = {
.waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
.rightClick('[data-path="Browser_E2E_Tests"]')
.click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
},
@ -81,13 +81,13 @@ module.exports = {
.pause(10000)
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000)
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000)
.perform((done) => {
if (runtimeBrowser === 'chrome') {
@ -101,6 +101,7 @@ module.exports = {
'Should open local filesystem explorer': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="filePanelFileExplorerTree"]')
.click('[data-id="remixUIWorkspaceExplorer"]')
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3)

@ -103,14 +103,18 @@ module.exports = {
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(10000)
.getText('[data-id="default_workspaceModalDialogModalBody-react"]', (result) => {
browser.assert.ok(result.value === 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Assert failed. Gist token error message not displayed.')
.perform((done) => {
browser.getText('[data-id="fileSystemModalDialogModalBody-react"]', (result) => {
console.log('result.value: ', result.value)
browser.assert.ok(result.value === 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Assert failed. Gist token error message not displayed.')
done()
})
})
.click('[data-id="default_workspace-modal-footer-ok-react"]')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
},
'Import From Gist For Valid Gist ID': function (browser: NightwatchBrowser) {

@ -153,7 +153,6 @@ function runTests (browser: NightwatchBrowser) {
.clickLaunchIcon('filePanel')
.waitForElementVisible('[data-path="folder1"]')
.click('[data-path="folder1"]')
.click('[data-path="folder1"]') // click twice because remixd does not return nested folder details after update
.waitForElementVisible('[data-path="folder1/contract1.sol"]')
.waitForElementVisible('[data-path="folder1/renamed_contract_' + browserName + '.sol"]') // check if renamed file is preset
.waitForElementNotPresent('[data-path="folder1/contract_' + browserName + '.sol"]') // check if renamed (old) file is not present

@ -32,9 +32,10 @@ module.exports = {
.click('*[data-id="verticalIconsKindsolidityUnitTesting"]')
.waitForElementPresent('*[data-id="testTabGenerateTestFile"]')
.click('*[data-id="testTabGenerateTestFile"]')
.waitForElementPresent('*[title="tests/simple_storage_test.sol"]')
.waitForElementPresent('*[title="default_workspace/tests/simple_storage_test.sol"]')
.clickLaunchIcon('filePanel')
.pause(10000)
.waitForElementPresent('[data-id="treeViewDivtreeViewItemtests"]')
.click('[data-id="treeViewDivtreeViewItemtests"]')
.openFile('tests/simple_storage_test.sol')
.removeFile('tests/simple_storage_test.sol', 'default_workspace')
},
@ -164,10 +165,9 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' })
.pause(5000)
.waitForElementPresent('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok')
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok')
.click('*[data-id="workspacesSelect"] option[value="workspace_new"]')
.waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]')
.execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() })
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]')
// end of creating
.clickLaunchIcon('solidityUnitTesting')
.pause(2000)
@ -193,8 +193,17 @@ module.exports = {
},
'Solidity Unit tests with hardhat console log': function (browser: NightwatchBrowser) {
const runtimeBrowser = browser.options.desiredCapabilities.browserName
browser
.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.perform((done) => {
if (runtimeBrowser !== 'chrome') {
browser.clickLaunchIcon('filePanel')
.waitForElementVisible('[data-id="treeViewLitreeViewItemtests"]')
}
done()
})
.addFile('tests/hhLogs_test.sol', sources[0]['tests/hhLogs_test.sol'])
.clickLaunchIcon('solidityUnitTesting')
.waitForElementVisible('*[id="singleTesttests/4_Ballot_test.sol"]', 60000)

@ -111,6 +111,7 @@ module.exports = {
.addFile('deployWithEthersJs.js', { content: deployWithEthersJs })
.openFile('deployWithEthersJs.js')
.pause(1000)
.click('[data-id="treeViewDivtreeViewItemcontracts"]')
.openFile('contracts/2_Owner.sol')
.clickLaunchIcon('solidity')
.click('*[data-id="compilerContainerCompileBtn"]') // compile Owner

@ -19,7 +19,7 @@ module.exports = {
browser
.pause(5000)
.refresh()
.pause(5000)
.pause(10000)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('contract Ballot {') !== -1, 'content doesn\'t include Ballot contract')
})
@ -35,24 +35,22 @@ module.exports = {
.clickLaunchIcon('filePanel')
.click('*[data-id="workspaceCreate"]') // create workspace_name
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name' })
.pause(1000)
.click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.pause(1000)
.addFile('test.sol', { content: 'test' })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
.click('*[data-id="workspaceCreate"]') // create workspace_name_1
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name_1' })
.waitForElementVisible('span[data-id="workspacesModalDialog-modal-footer-ok-react"]')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') })
.pause(2000)
.click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
@ -68,13 +66,13 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextRename"]')['value'] = 'workspace_name_renamed' })
.pause(2000)
.waitForElementPresent('span[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.pause(2000)
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]')
.pause(2000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
@ -84,10 +82,8 @@ module.exports = {
browser
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.click('*[data-id="workspaceDelete"]') // delete workspace_name_1
.waitForElementVisible('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span')
.pause(2000)
.click('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span')
.pause(2000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.end()
},

@ -464,13 +464,18 @@ class App {
console.log('couldn\'t register iframe plugins', e.message)
}
await appManager.activatePlugin(['theme', 'editor', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await appManager.activatePlugin(['editor'])
await appManager.activatePlugin(['theme', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs'])
await appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await appManager.activatePlugin(['home'])
await appManager.activatePlugin(['settings'])
await appManager.activatePlugin(['hiddenPanel', 'filePanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport'])
await appManager.registerContextMenuItems()
await appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport'])
appManager.on('filePanel', 'workspaceInitializationCompleted', async () => {
await appManager.registerContextMenuItems()
})
await appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation
appManager.on('editor', 'editorMounted', () => {
if (Array.isArray(workspace)) {

@ -75,8 +75,6 @@ class Editor extends Plugin {
}
}
this.el.gotoLine = (line) => this.gotoLine(line, 0)
this.renderComponent()
return this.el
}
@ -118,6 +116,7 @@ class Editor extends Plugin {
this.currentTheme = translateTheme(theme)
this.renderComponent()
})
this.renderComponent()
}
onDeactivation () {

@ -227,7 +227,7 @@ class DGitProvider extends Plugin {
const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.')
if (!permission) return false
if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.')
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, false)
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true)
const cmd = {
url: input.url,
@ -459,7 +459,7 @@ class DGitProvider extends Plugin {
if (!permission) return false
if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.')
const cid = cmd.cid
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, false)
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true)
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
let result
if (cmd.local) {

@ -456,7 +456,7 @@ class FileManager extends Plugin {
return this._deps.config.get('currentFile')
}
closeAllFiles () {
async closeAllFiles () {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('filesAllClosed')
this.events.emit('filesAllClosed')
@ -465,7 +465,7 @@ class FileManager extends Plugin {
}
}
closeFile (name) {
async closeFile (name) {
delete this.openedFiles[name]
if (!Object.keys(this.openedFiles).length) {
this._deps.config.set('currentFile', '')

@ -15,9 +15,9 @@ module.exports = class RemixDProvider extends FileProvider {
_registerEvent () {
var remixdEvents = ['connecting', 'connected', 'errored', 'closed']
remixdEvents.forEach((value) => {
this._appManager.on('remixd', value, (event) => {
this.event.emit(value, event)
remixdEvents.forEach((event) => {
this._appManager.on('remixd', event, (value) => {
this.event.emit(event, value)
})
})
@ -41,8 +41,18 @@ module.exports = class RemixDProvider extends FileProvider {
this.event.emit('fileRenamed', oldPath, newPath)
})
this._appManager.on('remixd', 'rootFolderChanged', () => {
this.event.emit('rootFolderChanged')
this._appManager.on('remixd', 'rootFolderChanged', (path) => {
this.event.emit('rootFolderChanged', path)
})
this._appManager.on('remixd', 'removed', (path) => {
this.event.emit('fileRemoved', path)
})
this._appManager.on('remixd', 'changed', (path) => {
this.get(path, (_error, content) => {
this.event.emit('fileExternallyChanged', path, content)
})
})
}
@ -57,7 +67,7 @@ module.exports = class RemixDProvider extends FileProvider {
}
preInit () {
this.event.emit('loading')
this.event.emit('loadingLocalhost')
}
init (cb) {
@ -66,6 +76,7 @@ module.exports = class RemixDProvider extends FileProvider {
.then((result) => {
this._isReady = true
this._readOnlyMode = result
this.event.emit('readOnlyModeChanged', result)
this._registerEvent()
this.event.emit('connected')
cb && cb()
@ -177,9 +188,7 @@ module.exports = class RemixDProvider extends FileProvider {
}
resolveDirectory (path, callback) {
var self = this
if (path[0] === '/') path = path.substring(1)
if (!path) return callback(null, { [self.type]: { } })
const unprefixedpath = this.removePrefix(path)
if (!this._isReady) return callback && callback('provider not ready')

@ -13,6 +13,7 @@ class WorkspaceFileProvider extends FileProvider {
}
setWorkspace (workspace) {
if (!workspace) return
workspace = workspace.replace(/^\/|\/$/g, '') // remove first and last slash
this.workspace = workspace
}
@ -30,7 +31,6 @@ class WorkspaceFileProvider extends FileProvider {
}
removePrefix (path) {
if (!this.workspace) this.createWorkspace()
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path
if (path.startsWith(this.workspace)) return path.replace(this.workspace, this.workspacesPath + '/' + this.workspace)
@ -51,7 +51,6 @@ class WorkspaceFileProvider extends FileProvider {
}
resolveDirectory (path, callback) {
if (!this.workspace) this.createWorkspace()
super.resolveDirectory(path, (error, files) => {
if (error) return callback(error)
const unscoped = {}
@ -76,13 +75,18 @@ class WorkspaceFileProvider extends FileProvider {
}
_normalizePath (path) {
if (!this.workspace) this.createWorkspace()
return path.replace(this.workspacesPath + '/' + this.workspace + '/', '')
}
createWorkspace (name) {
if (!name) name = 'default_workspace'
this.event.emit('createWorkspace', name)
async createWorkspace (name) {
try {
if (!name) name = 'default_workspace'
this.setWorkspace(name)
await super.createDir(name)
this.event.emit('createWorkspace', name)
} catch (e) {
throw new Error(e)
}
}
}

@ -3,18 +3,12 @@ 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 { bufferToHex, keccakFromString } from 'ethereumjs-util'
import { checkSpecialChars, checkSlash } from '../../lib/helper'
import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js')
const globalRegistry = require('../../global/registry')
const examples = require('../editor/examples')
const GistHandler = require('../../lib/gist-handler')
const QueryParams = require('../../lib/query-params')
const modalDialogCustom = require('../ui/modal-dialog-custom')
/*
Overview of APIs:
* fileManager: @args fileProviders (browser, shared-folder, swarm, github, etc ...) & config & editor
@ -35,8 +29,8 @@ const modalDialogCustom = require('../ui/modal-dialog-custom')
const profile = {
name: 'filePanel',
displayName: 'File explorers',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace', 'createWorkspace'],
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'],
events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp',
description: ' - ',
kind: 'fileexplorer',
@ -47,54 +41,33 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) {
super(profile)
this._components = {}
this._components.registry = globalRegistry
this._deps = {
fileProviders: this._components.registry.get('fileproviders').api,
fileManager: this._components.registry.get('filemanager').api
}
this.registry = globalRegistry
this.fileProviders = this.registry.get('fileproviders').api
this.fileManager = this.registry.get('filemanager').api
this.el = document.createElement('div')
this.el.setAttribute('id', 'fileExplorerView')
this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager)
this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle()
this.slitherHandle = new SlitherHandle()
this.registeredMenuItems = []
this.removedMenuItems = []
this.request = {}
this.workspaces = []
this.initialWorkspace = null
this.appManager = appManager
this.currentWorkspaceMetadata = {}
}
onActivation () {
this.renderComponent()
}
render () {
this.on('editor', 'editorMounted', () => this.initWorkspace().then(() => this.getWorkspaces()).catch(console.error))
return this.el
}
renderComponent () {
ReactDOM.render(
<Workspace
createWorkspace={this.createWorkspace.bind(this)}
renameWorkspace={this.renameWorkspace.bind(this)}
setWorkspace={this.setWorkspace.bind(this)}
workspaceRenamed={this.workspaceRenamed.bind(this)}
workspaceDeleted={this.workspaceDeleted.bind(this)}
workspaceCreated={this.workspaceCreated.bind(this)}
workspace={this._deps.fileProviders.workspace}
browser={this._deps.fileProviders.browser}
localhost={this._deps.fileProviders.localhost}
fileManager={this._deps.fileManager}
registry={this._components.registry}
plugin={this}
request={this.request}
workspaces={this.workspaces}
registeredMenuItems={this.registeredMenuItems}
removedMenuItems={this.removedMenuItems}
initialWorkspace={this.initialWorkspace}
/>
<FileSystemProvider plugin={this} />
, this.el)
}
@ -103,202 +76,103 @@ module.exports = class Filepanel extends ViewPlugin {
* @param callback (...args) => void
*/
registerContextMenuItem (item) {
if (!item) throw new Error('Invalid register context menu argument')
if (!item.name || !item.id) throw new Error('Item name and id is mandatory')
if (!item.type && !item.path && !item.extension && !item.pattern) throw new Error('Invalid file matching criteria provided')
if (this.registeredMenuItems.filter((o) => {
return o.id === item.id && o.name === item.name
}).length) throw new Error(`Action ${item.name} already exists on ${item.id}`)
this.registeredMenuItems = [...this.registeredMenuItems, item]
this.removedMenuItems = this.removedMenuItems.filter(menuItem => item.id !== menuItem.id)
this.renderComponent()
return new Promise((resolve, reject) => {
this.emit('registerContextMenuItemReducerEvent', item, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
removePluginActions (plugin) {
this.registeredMenuItems = this.registeredMenuItems.filter((item) => {
if (item.id !== plugin.name || item.sticky === true) return true
else {
this.removedMenuItems.push(item)
return false
}
return new Promise((resolve, reject) => {
this.emit('removePluginActionsReducerEvent', plugin, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
this.renderComponent()
}
async getCurrentWorkspace () {
return await this.request.getCurrentWorkspace()
getCurrentWorkspace () {
return this.currentWorkspaceMetadata
}
async getWorkspaces () {
const result = new Promise((resolve, reject) => {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
this._deps.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 + '/', '')))
})
})
try {
this.workspaces = await result
} 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)
}
this.renderComponent()
getWorkspaces () {
return this.workspaces
}
async initWorkspace () {
this.renderComponent()
const queryParams = new QueryParams()
const gistHandler = new GistHandler()
const params = queryParams.get()
// get the file from gist
let loadedFromGist = false
if (params.gist) {
await this.processCreateWorkspace('gist-sample')
this._deps.fileProviders.workspace.setWorkspace('gist-sample')
this.initialWorkspace = 'gist-sample'
loadedFromGist = gistHandler.loadFromGist(params, this._deps.fileManager)
}
if (loadedFromGist) return
if (params.code || params.url) {
try {
await this.processCreateWorkspace('code-sample')
this._deps.fileProviders.workspace.setWorkspace('code-sample')
let path = ''
let content = ''
if (params.code) {
var hash = bufferToHex(keccakFromString(params.code))
path = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol'
content = atob(params.code)
await this._deps.fileProviders.workspace.set(path, content)
}
if (params.url) {
const data = await this.call('contentImport', 'resolve', params.url)
path = data.cleanUrl
content = data.content
await this._deps.fileProviders.workspace.set(path, content)
}
this.initialWorkspace = 'code-sample'
await this._deps.fileManager.openFile(path)
} catch (e) {
console.error(e)
}
return
}
setWorkspaces (workspaces) {
this.workspaces = workspaces
}
const self = this
this.appManager.on('manager', 'pluginDeactivated', self.removePluginActions.bind(this))
// insert example contracts if there are no files to show
createNewFile () {
return new Promise((resolve, reject) => {
this._deps.fileProviders.browser.resolveDirectory('/', async (error, filesList) => {
if (error) return reject(error)
if (Object.keys(filesList).length === 0) {
await this.createWorkspace('default_workspace')
resolve('default_workspace')
} else {
this._deps.fileProviders.browser.resolveDirectory('.workspaces', async (error, filesList) => {
if (error) return reject(error)
if (Object.keys(filesList).length > 0) {
const workspacePath = Object.keys(filesList)[0].split('/').filter(val => val)
const workspaceName = workspacePath[workspacePath.length - 1]
const provider = this.fileManager.currentFileProvider()
const dir = provider.workspace || '/'
this._deps.fileProviders.workspace.setWorkspace(workspaceName)
return resolve(workspaceName)
}
return reject(new Error('Can\'t find available workspace.'))
})
}
this.emit('createNewFileInputReducerEvent', dir, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
async createNewFile () {
return await this.request.createNewFile()
}
uploadFile (target) {
return new Promise((resolve, reject) => {
const provider = this.fileManager.currentFileProvider()
const dir = provider.workspace || '/'
async uploadFile (event) {
return await this.request.uploadFile(event)
return this.emit('uploadFileReducerEvent', dir, target, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
async processCreateWorkspace (name) {
const workspaceProvider = this._deps.fileProviders.workspace
const browserProvider = this._deps.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath
const workspaceRootPathExists = await browserProvider.exists(workspaceRootPath)
const workspacePathExists = await browserProvider.exists(workspacePath)
if (!workspaceRootPathExists) browserProvider.createDir(workspaceRootPath)
if (!workspacePathExists) browserProvider.createDir(workspacePath)
createWorkspace (workspaceName, isEmpty) {
return new Promise((resolve, reject) => {
this.emit('createWorkspaceReducerEvent', workspaceName, isEmpty, (err, data) => {
if (err) reject(err)
else resolve(data || true)
})
})
}
async workspaceExists (name) {
const workspaceProvider = this._deps.fileProviders.workspace
const browserProvider = this._deps.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
return browserProvider.exists(workspacePath)
renameWorkspace (oldName, workspaceName) {
return new Promise((resolve, reject) => {
this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => {
if (err) reject(err)
else resolve(data || true)
})
})
}
async createWorkspace (workspaceName, setDefaults = true) {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
else {
const workspaceProvider = this._deps.fileProviders.workspace
await this.processCreateWorkspace(workspaceName)
workspaceProvider.setWorkspace(workspaceName)
await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace
if (setDefaults) {
for (const file in examples) {
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}
}
}
deleteWorkspace (workspaceName) {
return new Promise((resolve, reject) => {
this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => {
if (err) reject(err)
else resolve(data || true)
})
})
}
async renameWorkspace (oldName, workspaceName) {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
const browserProvider = this._deps.fileProviders.browser
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
browserProvider.rename('browser/' + workspacesPath + '/' + oldName, 'browser/' + workspacesPath + '/' + workspaceName, true)
}
setWorkspace (workspace) {
const workspaceProvider = this.fileProviders.workspace
/** these are called by the react component, action is already finished whent it's called */
async setWorkspace (workspace, setEvent = true) {
if (workspace.isLocalhost) {
this.call('manager', 'activatePlugin', 'remixd')
} else if (await this.call('manager', 'isActive', 'remixd')) {
this.call('manager', 'deactivatePlugin', 'remixd')
}
if (setEvent) {
this._deps.fileManager.setMode(workspace.isLocalhost ? 'localhost' : 'browser')
this.emit('setWorkspace', workspace)
}
this.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` }
this.emit('setWorkspace', workspace)
}
workspaceRenamed (workspace) {
this.emit('renameWorkspace', workspace)
workspaceRenamed (oldName, workspaceName) {
this.emit('workspaceRenamed', oldName, workspaceName)
}
workspaceDeleted (workspace) {
this.emit('deleteWorkspace', workspace)
this.emit('workspaceDeleted', workspace)
}
workspaceCreated (workspace) {
this.emit('createWorkspace', workspace)
this.emit('workspaceCreated', workspace)
}
/** end section */
}

@ -44,19 +44,32 @@ export class TabProxy extends Plugin {
fileManager.events.on('fileRemoved', (name) => {
const workspace = this.fileManager.currentWorkspace()
workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name)
if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
})
fileManager.events.on('fileClosed', (name) => {
const workspace = this.fileManager.currentWorkspace()
workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name)
if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
})
fileManager.events.on('currentFileChanged', (file) => {
const workspace = this.fileManager.currentWorkspace()
if (workspace) {
if (this.fileManager.mode === 'browser') {
const workspacePath = workspace + '/' + file
if (this._handlers[workspacePath]) {
@ -72,7 +85,7 @@ export class TabProxy extends Plugin {
this.event.emit('closeFile', file)
})
} else {
const path = this.fileManager.mode + '/' + file
const path = file.startsWith(this.fileManager.mode + '/') ? file : this.fileManager.mode + '/' + file
if (this._handlers[path]) {
this._view.filetabs.activateTab(path)
@ -92,7 +105,7 @@ export class TabProxy extends Plugin {
fileManager.events.on('fileRenamed', (oldName, newName, isFolder) => {
const workspace = this.fileManager.currentWorkspace()
if (workspace) {
if (this.fileManager.mode === 'browser') {
if (isFolder) {
for (const tab of this.loadedTabs) {
if (tab.name.indexOf(workspace + '/' + oldName + '/') === 0) {
@ -115,7 +128,7 @@ export class TabProxy extends Plugin {
return
}
// should change the tab title too
this.renameTab(this.fileManager.mode + '/' + oldName, workspace + '/' + newName)
this.renameTab(this.fileManager.mode + '/' + oldName, this.fileManager.mode + '/' + newName)
}
})

@ -115,14 +115,16 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
async onActivation () {
super.onActivation()
this.call('filePanel', 'registerContextMenuItem', {
id: 'solidity',
name: 'compileFile',
label: 'Compile',
type: [],
extension: ['.sol'],
path: [],
pattern: []
this.on('filePanel', 'workspaceInitializationCompleted', () => {
this.call('filePanel', 'registerContextMenuItem', {
id: 'solidity',
name: 'compileFile',
label: 'Compile',
type: [],
extension: ['.sol'],
path: [],
pattern: []
})
})
try {
this.currentFile = await this.call('fileManager', 'file')

@ -81,7 +81,7 @@ commander
const compVersion = commander.compiler
const baseURL = 'https://binaries.soliditylang.org/wasm/'
const response: AxiosResponse = await axios.get(baseURL + 'list.json')
const { releases, latestRelease } = response.data
const { releases, latestRelease } = response.data as { releases: string[], latestRelease: string }
const compString = releases ? releases[compVersion] : null
if (!compString) {
log.error(`No compiler found in releases with version ${compVersion}`)

@ -1,4 +0,0 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -1,19 +0,0 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "../../../.eslintrc",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
}
}

@ -1,7 +0,0 @@
# remix-ui-file-explorer
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remix-ui-file-explorer` to execute the unit tests via [Jest](https://jestjs.io).

@ -1 +0,0 @@
export * from './lib/file-explorer'

@ -1,384 +0,0 @@
import React from 'react'
import { File } from '../types'
import { extractNameFromKey, extractParentFromKey } from '../utils'
const queuedEvents = []
const pendingEvents = {}
let provider = null
let plugin = null
let dispatch: React.Dispatch<any> = null
export const fetchDirectoryError = (error: any) => {
return {
type: 'FETCH_DIRECTORY_ERROR',
payload: error
}
}
export const fetchDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_DIRECTORY_REQUEST',
payload: promise
}
}
export const fetchDirectorySuccess = (path: string, files: File[]) => {
return {
type: 'FETCH_DIRECTORY_SUCCESS',
payload: { path, files }
}
}
export const fileSystemReset = () => {
return {
type: 'FILESYSTEM_RESET'
}
}
const normalize = (parent, filesList, newInputType?: string): 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)
}
const fetchDirectoryContent = async (provider, folderPath: string, newInputType?: string): Promise<any> => {
return new Promise((resolve) => {
provider.resolveDirectory(folderPath, (error, fileTree) => {
if (error) console.error(error)
const files = normalize(folderPath, fileTree, newInputType)
resolve({ [extractNameFromKey(folderPath)]: files })
})
})
}
export const fetchDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path)
dispatch(fetchDirectoryRequest(promise))
promise.then((files) => {
dispatch(fetchDirectorySuccess(path, files))
}).catch((error) => {
dispatch(fetchDirectoryError({ error }))
})
return promise
}
export const resolveDirectoryError = (error: any) => {
return {
type: 'RESOLVE_DIRECTORY_ERROR',
payload: error
}
}
export const resolveDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'RESOLVE_DIRECTORY_REQUEST',
payload: promise
}
}
export const resolveDirectorySuccess = (path: string, files: File[]) => {
return {
type: 'RESOLVE_DIRECTORY_SUCCESS',
payload: { path, files }
}
}
export const resolveDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path)
dispatch(resolveDirectoryRequest(promise))
promise.then((files) => {
dispatch(resolveDirectorySuccess(path, files))
}).catch((error) => {
dispatch(resolveDirectoryError({ error }))
})
return promise
}
export const fetchProviderError = (error: any) => {
return {
type: 'FETCH_PROVIDER_ERROR',
payload: error
}
}
export const fetchProviderRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_PROVIDER_REQUEST',
payload: promise
}
}
export const fetchProviderSuccess = (provider: any) => {
return {
type: 'FETCH_PROVIDER_SUCCESS',
payload: provider
}
}
export const fileAddedSuccess = (path: string, files) => {
return {
type: 'FILE_ADDED',
payload: { path, files }
}
}
export const folderAddedSuccess = (path: string, files) => {
return {
type: 'FOLDER_ADDED',
payload: { path, files }
}
}
export const fileRemovedSuccess = (path: string, removePath: string) => {
return {
type: 'FILE_REMOVED',
payload: { path, removePath }
}
}
export const fileRenamedSuccess = (path: string, removePath: string, files) => {
return {
type: 'FILE_RENAMED',
payload: { path, removePath, files }
}
}
export const init = (fileProvider, filePanel, registry) => (reducerDispatch: React.Dispatch<any>) => {
provider = fileProvider
plugin = filePanel
dispatch = reducerDispatch
if (provider) {
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 = registry.get('config').api
const editor = 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))
} else {
dispatch(fetchProviderError('No provider available'))
}
}
export const setCurrentWorkspace = (name: string) => {
return {
type: 'SET_CURRENT_WORKSPACE',
payload: name
}
}
export const addInputFieldSuccess = (path: string, files: File[]) => {
return {
type: 'ADD_INPUT_FIELD',
payload: { path, files }
}
}
export const addInputField = (provider, type: string, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path, type)
promise.then((files) => {
dispatch(addInputFieldSuccess(path, files))
}).catch((error) => {
console.error(error)
})
return promise
}
export const removeInputFieldSuccess = (path: string) => {
return {
type: 'REMOVE_INPUT_FIELD',
payload: { path }
}
}
export const removeInputField = (path: string) => (dispatch: React.Dispatch<any>) => {
return dispatch(removeInputFieldSuccess(path))
}
export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => {
return {
type: 'DISPLAY_NOTIFICATION',
payload: { title, message, labelOk, labelCancel, actionOk, actionCancel }
}
}
export const hideNotification = () => {
return {
type: 'DISPLAY_NOTIFICATION'
}
}
export const closeNotificationModal = () => (dispatch: React.Dispatch<any>) => {
dispatch(hideNotification())
}
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
}
}

File diff suppressed because it is too large Load Diff

@ -1,348 +0,0 @@
import * as _ from 'lodash'
import { extractNameFromKey } from '../utils'
interface Action {
type: string;
payload: Record<string, any>;
}
export const fileSystemInitialState = {
files: {
files: [],
expandPath: [],
blankPath: null,
isRequesting: false,
isSuccessful: false,
error: null
},
provider: {
provider: null,
isRequesting: false,
isSuccessful: false,
error: null
},
notification: {
title: null,
message: null,
actionOk: () => {},
actionCancel: () => {},
labelOk: null,
labelCancel: null
}
}
export const fileSystemReducer = (state = fileSystemInitialState, action: Action) => {
switch (action.type) {
case 'FETCH_DIRECTORY_REQUEST': {
return {
...state,
files: {
...state.files,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'FETCH_DIRECTORY_SUCCESS': {
return {
...state,
files: {
...state.files,
files: action.payload.files,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FETCH_DIRECTORY_ERROR': {
return {
...state,
files: {
...state.files,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'RESOLVE_DIRECTORY_REQUEST': {
return {
...state,
files: {
...state.files,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'RESOLVE_DIRECTORY_SUCCESS': {
return {
...state,
files: {
...state.files,
files: resolveDirectory(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'RESOLVE_DIRECTORY_ERROR': {
return {
...state,
files: {
...state.files,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'FETCH_PROVIDER_REQUEST': {
return {
...state,
provider: {
...state.provider,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'FETCH_PROVIDER_SUCCESS': {
return {
...state,
provider: {
...state.provider,
provider: action.payload,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FETCH_PROVIDER_ERROR': {
return {
...state,
provider: {
...state.provider,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'ADD_INPUT_FIELD': {
return {
...state,
files: {
...state.files,
files: addInputField(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
blankPath: action.payload.path,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'REMOVE_INPUT_FIELD': {
return {
...state,
files: {
...state.files,
files: removeInputField(state.provider.provider, state.files.blankPath, state.files.files),
blankPath: null,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_ADDED': {
return {
...state,
files: {
...state.files,
files: fileAdded(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FOLDER_ADDED': {
return {
...state,
files: {
...state.files,
files: folderAdded(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_REMOVED': {
return {
...state,
files: {
...state.files,
files: fileRemoved(state.provider.provider, action.payload.path, action.payload.removePath, state.files.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_RENAMED': {
return {
...state,
files: {
...state.files,
files: fileRenamed(state.provider.provider, action.payload.path, action.payload.removePath, state.files.files, action.payload.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'DISPLAY_NOTIFICATION': {
return {
...state,
notification: {
title: action.payload.title,
message: action.payload.message,
actionOk: action.payload.actionOk || fileSystemInitialState.notification.actionOk,
actionCancel: action.payload.actionCancel || fileSystemInitialState.notification.actionCancel,
labelOk: action.payload.labelOk,
labelCancel: action.payload.labelCancel
}
}
}
case 'HIDE_NOTIFICATION': {
return {
...state,
notification: fileSystemInitialState.notification
}
}
default:
throw new Error()
}
}
const resolveDirectory = (provider, path: string, files, content) => {
const root = provider.workspace || provider.type
if (path === root) return { [root]: { ...content[root], ...files[root] } }
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: { ...content[pathArr[pathArr.length - 1]], ...(prevFiles ? prevFiles.child : {}) }
})
return files
}
const removePath = (root, path: string, pathName, files) => {
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child && prevFiles.child[pathName] && delete prevFiles.child[pathName]
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: prevFiles ? prevFiles.child : {}
})
}
return files
}
const addInputField = (provider, path: string, files, content) => {
const root = provider.workspace || provider.type || ''
if (path === root) return { [root]: { ...content[root], ...files[root] } }
const result = resolveDirectory(provider, path, files, content)
return result
}
const removeInputField = (provider, path: string, files) => {
const root = provider.workspace || provider.type || ''
if (path === root) {
delete files[root][path + '/' + 'blank']
return files
}
return removePath(root, path, path + '/' + 'blank', files)
}
const fileAdded = (provider, path: string, files, content) => {
return resolveDirectory(provider, path, files, content)
}
const folderAdded = (provider, path: string, files, content) => {
return resolveDirectory(provider, path, files, content)
}
const fileRemoved = (provider, path: string, removedPath: string, files) => {
const root = provider.workspace || provider.type || ''
if (path === root) {
delete files[root][removedPath]
return files
}
return removePath(root, path, extractNameFromKey(removedPath), files)
}
const fileRenamed = (provider, path: string, removePath: string, files, content) => {
const root = provider.workspace || provider.type || ''
if (path === root) {
const allFiles = { [root]: { ...content[root], ...files[root] } }
delete allFiles[root][extractNameFromKey(removePath) || removePath]
return allFiles
}
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
delete prevFiles.child[extractNameFromKey(removePath)]
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child }
})
return files
}

@ -1,59 +0,0 @@
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
export type MenuItems = action[] // eslint-disable-line no-use-before-define
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
registry: any,
filesProvider: any,
menuItems?: string[],
plugin: any,
focusRoot: boolean,
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement,
}
export interface File {
path: string,
name: string,
isDirectory: boolean,
type: string,
child?: File[]
}
export interface FileExplorerMenuProps {
title: string,
menuItems: string[],
fileManager: any,
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
publishToGist: (path?: string) => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void
}
export type action = { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string, multiselect: boolean, label: string }
export interface FileExplorerContextMenuProps {
actions: action[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => void,
hideContextMenu: () => void,
publishToGist?: (path?: string, type?: string) => void,
pushChangesToGist?: (path?: string, type?: string) => void,
publishFolderToGist?: (path?: string, type?: string) => void,
publishFileToGist?: (path?: string, type?: string) => void,
runScript?: (path: string) => void,
emit?: (cmd: customAction) => void,
pageX: number,
pageY: number,
path: string,
type: string,
focus: {key:string, type:string}[],
onMouseOver?: (...args) => void,
copy?: (path: string, type: string) => void,
paste?: (destination: string, type: string) => void
}

@ -1,13 +0,0 @@
export const extractNameFromKey = (key: string): string => {
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
}
export const extractParentFromKey = (key: string):string => {
if (!key) return
const keyPath = key.split('/')
keyPath.pop()
return keyPath.join('/')
}

@ -1,13 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -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,63 @@
export const extractNameFromKey = (key: string): string => {
if (!key) return
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
}
export const extractParentFromKey = (key: string):string => {
if (!key) return
const keyPath = key.split('/')
keyPath.pop()
return keyPath.join('/')
}
export const checkSpecialChars = (name: string) => {
return name.match(/[:*?"<>\\'|]/) != null
}
export const checkSlash = (name: string) => {
return name.match(/\//) != null
}
export const createNonClashingNameAsync = async (name: string, fileManager, prefix = '') => {
if (!name) name = 'Undefined'
let _counter
let ext = 'sol'
const reg = /(.*)\.([^.]+)/g
const split = reg.exec(name)
if (split) {
name = split[1]
ext = split[2]
}
let exist = true
do {
const isDuplicate = await fileManager.exists(name + _counter + prefix + '.' + ext)
if (isDuplicate) _counter = (_counter | 0) + 1
else exist = false
} while (exist)
const counter = _counter || ''
return name + counter + prefix + '.' + ext
}
export const joinPath = (...paths) => {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
if (paths.length === 1) return paths[0]
return paths.join('/')
}
export const getPathIcon = (path: string) => {
return path.endsWith('.txt')
? 'far fa-file-alt' : path.endsWith('.md')
? 'far fa-file-alt' : path.endsWith('.sol')
? 'fak fa-solidity-mono' : path.endsWith('.js')
? 'fab fa-js' : path.endsWith('.json')
? 'fas fa-brackets-curly' : path.endsWith('.vy')
? 'fak fa-vyper-mono' : path.endsWith('.lex')
? 'fak fa-lexon' : path.endsWith('.contract')
? 'fab fa-ethereum' : 'far fa-file'
}

@ -1,11 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [

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

@ -110,7 +110,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input
className="form-control"
onChange={e => setName(e.target.value)}
value={ name}
value={ name || '' }
id="plugin-name"
data-id="localPluginName"
placeholder="Should be camelCase" />
@ -120,7 +120,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input
className="form-control"
onChange={e => setDisplayName(e.target.value)}
value={ displayName }
value={ displayName || '' }
id="plugin-displayname"
data-id="localPluginDisplayName"
placeholder="Name in the header" />
@ -130,7 +130,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input
className="form-control"
onChange={e => setMethods(e.target.value)}
value={ methods }
value={ methods || '' }
id="plugin-methods"
data-id="localPluginMethods"
placeholder="Methods" />
@ -140,7 +140,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input
className="form-control"
onChange={e => setCanactivate(e.target.value)}
value={ canactivate }
value={ canactivate || '' }
id="plugin-canactivate"
data-id="localPluginCanActivate"
placeholder="Plugin names" />
@ -151,7 +151,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input
className="form-control"
onChange={e => setUrl(e.target.value)}
value={ url }
value={ url || '' }
id="plugin-url"
data-id="localPluginUrl"
placeholder="ex: https://localhost:8000" />

@ -6,7 +6,8 @@ import './toaster.css'
/* eslint-disable-next-line */
export interface ToasterProps {
message: string
timeOut?: number
timeOut?: number,
handleHide?: () => void
}
export const Toaster = (props: ToasterProps) => {
@ -59,6 +60,7 @@ export const Toaster = (props: ToasterProps) => {
if (state.timeOutId) {
clearTimeout(state.timeOutId)
}
props.handleHide && props.handleHide()
setState(prevState => {
return { ...prevState, message: '', hide: true, hiding: false, timeOutId: null, showModal: false }
})

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

@ -0,0 +1,187 @@
import { extractParentFromKey } from '@remix-ui/helper'
import React from 'react'
import { action } from '../types'
import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, loadLocalhostError, loadLocalhostRequest, loadLocalhostSuccess, removeContextMenuItem, rootFolderChangedSuccess, setContextMenuItem, setMode, setReadOnlyMode } from './payload'
import { addInputField, createWorkspace, deleteWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from './workspace'
const LOCALHOST = ' - connect to localhost - '
let plugin, dispatch: React.Dispatch<any>
export const listenOnPluginEvents = (filePanelPlugin) => {
plugin = filePanelPlugin
plugin.on('filePanel', 'createWorkspaceReducerEvent', (name: string, isEmpty = false, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
createWorkspace(name, isEmpty, cb)
})
plugin.on('filePanel', 'renameWorkspaceReducerEvent', (oldName: string, workspaceName: string, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
renameWorkspace(oldName, workspaceName, cb)
})
plugin.on('filePanel', 'deleteWorkspaceReducerEvent', (workspaceName: string, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
deleteWorkspace(workspaceName, cb)
})
plugin.on('filePanel', 'registerContextMenuItemReducerEvent', (item: action, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
registerContextMenuItem(item, cb)
})
plugin.on('filePanel', 'removePluginActionsReducerEvent', (plugin, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
removePluginActions(plugin, cb)
})
plugin.on('filePanel', 'createNewFileInputReducerEvent', (path, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
addInputField('file', path, cb)
})
plugin.on('filePanel', 'uploadFileReducerEvent', (dir: string, target, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
uploadFile(target, dir, cb)
})
plugin.on('remixd', 'rootFolderChanged', async (path: string) => {
rootFolderChanged(path)
})
}
export const listenOnProviderEvents = (provider) => (reducerDispatch: React.Dispatch<any>) => {
dispatch = reducerDispatch
provider.event.on('fileAdded', (filePath: string) => {
fileAdded(filePath)
})
provider.event.on('folderAdded', (folderPath: string) => {
if (folderPath.indexOf('/.workspaces') === 0) return
folderAdded(folderPath)
})
provider.event.on('fileRemoved', (removePath: string) => {
fileRemoved(removePath)
})
provider.event.on('fileRenamed', (oldPath: string) => {
fileRenamed(oldPath)
})
provider.event.on('disconnected', async () => {
plugin.fileManager.setMode('browser')
dispatch(setMode('browser'))
dispatch(loadLocalhostError('Remixd disconnected!'))
const workspaceProvider = plugin.fileProviders.workspace
await switchToWorkspace(workspaceProvider.workspace)
})
provider.event.on('connected', () => {
plugin.fileManager.setMode('localhost')
dispatch(setMode('localhost'))
fetchWorkspaceDirectory('/')
dispatch(loadLocalhostSuccess())
})
provider.event.on('loadingLocalhost', async () => {
await switchToWorkspace(LOCALHOST)
dispatch(loadLocalhostRequest())
})
provider.event.on('fileExternallyChanged', (path: string, content: string) => {
const config = plugin.registry.get('config').api
const editor = plugin.registry.get('editor').api
if (config.get('currentFile') === path && editor.currentContent() !== content) {
if (provider.isReadOnly(path)) return editor.setText(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(content)
}
))
}
})
provider.event.on('fileRenamedError', () => {
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel'))
})
provider.event.on('readOnlyModeChanged', (mode: boolean) => {
dispatch(setReadOnlyMode(mode))
})
}
const registerContextMenuItem = (item: action, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
if (!item) {
cb && cb(new Error('Invalid register context menu argument'))
return dispatch(displayPopUp('Invalid register context menu argument'))
}
if (!item.name || !item.id) {
cb && cb(new Error('Item name and id is mandatory'))
return dispatch(displayPopUp('Item name and id is mandatory'))
}
if (!item.type && !item.path && !item.extension && !item.pattern) {
cb && cb(new Error('Invalid file matching criteria provided'))
return dispatch(displayPopUp('Invalid file matching criteria provided'))
}
dispatch(setContextMenuItem(item))
cb && cb(null, item)
}
const removePluginActions = (plugin, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
dispatch(removeContextMenuItem(plugin))
cb && cb(null, true)
}
const fileAdded = async (filePath: string) => {
await dispatch(fileAddedSuccess(filePath))
if (filePath.includes('_test.sol')) {
plugin.emit('newTestFileCreated', filePath)
}
}
const folderAdded = async (folderPath: string) => {
const provider = plugin.fileManager.currentFileProvider()
const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || ''
const promise = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
promise.then((files) => {
folderPath = folderPath.replace(/^\/+/, '')
dispatch(folderAddedSuccess(path, folderPath, files))
}).catch((error) => {
console.error(error)
})
return promise
}
const fileRemoved = async (removePath: string) => {
await dispatch(fileRemovedSuccess(removePath))
}
const fileRenamed = async (oldPath: string) => {
const provider = plugin.fileManager.currentFileProvider()
const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || ''
const promise = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
promise.then((files) => {
dispatch(fileRenamedSuccess(path, oldPath, files))
}).catch((error) => {
console.error(error)
})
}
const rootFolderChanged = async (path) => {
await dispatch(rootFolderChangedSuccess(path))
}

@ -0,0 +1,332 @@
import React from 'react'
import { extractNameFromKey, createNonClashingNameAsync } from '@remix-ui/helper'
import Gists from 'gists'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type'
import { displayNotification, displayPopUp, fetchDirectoryError, fetchDirectoryRequest, fetchDirectorySuccess, focusElement, fsInitializationCompleted, hidePopUp, removeInputFieldSuccess, setCurrentWorkspace, setExpandPath, setMode, setWorkspaces } from './payload'
import { listenOnPluginEvents, listenOnProviderEvents } from './events'
import { createWorkspaceTemplate, getWorkspaces, loadWorkspacePreset, setPlugin } from './workspace'
export * from './events'
export * from './workspace'
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params')
const queryParams = new QueryParams()
let plugin, dispatch: React.Dispatch<any>
export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch<any>) => {
if (filePanelPlugin) {
plugin = filePanelPlugin
dispatch = reducerDispatch
setPlugin(plugin, dispatch)
const workspaceProvider = filePanelPlugin.fileProviders.workspace
const localhostProvider = filePanelPlugin.fileProviders.localhost
const params = queryParams.get()
const workspaces = await getWorkspaces() || []
dispatch(setWorkspaces(workspaces))
if (params.gist) {
await createWorkspaceTemplate('gist-sample', 'gist-template')
plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('gist-sample'))
await loadWorkspacePreset('gist-template')
} else if (params.code || params.url) {
await createWorkspaceTemplate('code-sample', 'code-template')
plugin.setWorkspace({ name: 'code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('code-sample'))
const filePath = await loadWorkspacePreset('code-template')
plugin.on('editor', 'editorMounted', () => plugin.fileManager.openFile(filePath))
} else {
if (workspaces.length === 0) {
await createWorkspaceTemplate('default_workspace', 'default-template')
plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false })
dispatch(setCurrentWorkspace('default_workspace'))
await loadWorkspacePreset('default-template')
} else {
if (workspaces.length > 0) {
workspaceProvider.setWorkspace(workspaces[workspaces.length - 1])
plugin.setWorkspace({ name: workspaces[workspaces.length - 1], isLocalhost: false })
dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1]))
}
}
}
listenOnPluginEvents(plugin)
listenOnProviderEvents(workspaceProvider)(dispatch)
listenOnProviderEvents(localhostProvider)(dispatch)
dispatch(setMode('browser'))
plugin.setWorkspaces(await getWorkspaces())
dispatch(fsInitializationCompleted())
plugin.emit('workspaceInitializationCompleted')
}
}
export const fetchDirectory = async (path: string) => {
const provider = plugin.fileManager.currentFileProvider()
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
}
export const removeInputField = async (path: string) => {
dispatch(removeInputFieldSuccess(path))
}
export const publishToGist = async (path?: string, type?: string) => {
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
const folder = path || '/'
const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null
try {
const packaged = await packageGistFiles(folder)
// check for token
const config = plugin.registry.get('config').api
const accessToken = config.get('settings/gist-access-token')
if (!accessToken) {
dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => {}))
} else {
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist='
const gists = new Gists({ token: accessToken })
if (id) {
const originalFileList = await getOriginalFiles(id)
// Telling the GIST API to remove files
const updatedFileList = Object.keys(packaged)
const allItems = Object.keys(originalFileList)
.filter(fileName => updatedFileList.indexOf(fileName) === -1)
.reduce((acc, deleteFileName) => ({
...acc,
[deleteFileName]: null
}), originalFileList)
// adding new files
updatedFileList.forEach((file) => {
const _items = file.split('/')
const _fileName = _items[_items.length - 1]
allItems[_fileName] = packaged[file]
})
dispatch(displayPopUp('Saving gist (' + id + ') ...'))
gists.edit({
description: description,
public: true,
files: allItems,
id: id
}, (error, result) => {
handleGistResponse(error, result)
if (!error) {
for (const key in allItems) {
if (allItems[key] === null) delete allItems[key]
}
}
})
} else {
// id is not existing, need to create a new gist
dispatch(displayPopUp('Creating a new gist ...'))
gists.create({
description: description,
public: true,
files: packaged
}, (error, result) => {
handleGistResponse(error, result)
})
}
}
} catch (error) {
console.log(error)
dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {}))
}
}
export const clearPopUp = async () => {
dispatch(hidePopUp())
}
export const createNewFile = async (path: string, rootDir: string) => {
const fileManager = plugin.fileManager
const newName = await createNonClashingNameAsync(path, fileManager)
const createFile = await fileManager.writeFile(newName, '')
if (!createFile) {
return dispatch(displayPopUp('Failed to create file ' + newName))
} else {
const path = newName.indexOf(rootDir + '/') === 0 ? newName.replace(rootDir + '/', '') : newName
await fileManager.open(path)
setFocusElement([{ key: path, type: 'file' }])
}
}
export const setFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => {
dispatch(focusElement(elements))
}
export const createNewFolder = async (path: string, rootDir: string) => {
const fileManager = plugin.fileManager
const dirName = path + '/'
const exists = await fileManager.exists(dirName)
if (exists) {
return dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => {}))
}
await fileManager.mkdir(dirName)
path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path
dispatch(focusElement([{ key: path, type: 'folder' }]))
}
export const deletePath = async (path: string[]) => {
const fileManager = plugin.fileManager
for (const p of path) {
try {
await fileManager.remove(p)
} catch (e) {
const isDir = await fileManager.isDirectory(p)
dispatch(displayPopUp(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`))
}
}
}
export const renamePath = async (oldPath: string, newPath: string) => {
const fileManager = plugin.fileManager
const exists = await fileManager.exists(newPath)
if (exists) {
dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => {}))
} else {
await fileManager.rename(oldPath, newPath)
}
}
export const copyFile = async (src: string, dest: string) => {
const fileManager = plugin.fileManager
try {
fileManager.copyFile(src, dest)
} catch (error) {
dispatch(displayPopUp('Oops! An error ocurred while performing copyFile operation.' + error))
}
}
export const copyFolder = async (src: string, dest: string) => {
const fileManager = plugin.fileManager
try {
fileManager.copyDir(src, dest)
} catch (error) {
dispatch(displayPopUp('Oops! An error ocurred while performing copyDir operation.' + error))
}
}
export const runScript = async (path: string) => {
const provider = plugin.fileManager.currentFileProvider()
provider.get(path, (error, content: string) => {
if (error) {
return dispatch(displayPopUp(error))
}
plugin.call('scriptRunner', 'execute', content)
})
}
export const emitContextMenuEvent = async (cmd: customAction) => {
plugin.call(cmd.id, cmd.name, cmd)
}
export const handleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => {
plugin.fileManager.open(path)
dispatch(focusElement([{ key: path, type }]))
}
export const handleExpandPath = (paths: string[]) => {
dispatch(setExpandPath(paths))
}
const packageGistFiles = (directory) => {
return new Promise((resolve, reject) => {
const workspaceProvider = plugin.fileProviders.workspace
const isFile = workspaceProvider.isFile(directory)
const ret = {}
if (isFile) {
try {
workspaceProvider.get(directory, (error, content) => {
if (error) throw new Error('An error ocurred while getting file content. ' + directory)
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
directory = directory.replace(/\//g, '...')
ret[directory] = { content }
return resolve(ret)
})
} catch (e) {
return reject(e)
}
} else {
try {
(async () => {
await workspaceProvider.copyFolderToJson(directory, ({ path, content }) => {
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
if (path.indexOf('gist-') === 0) {
path = path.split('/')
path.shift()
path = path.join('/')
}
path = path.replace(/\//g, '...')
ret[path] = { content }
})
resolve(ret)
})()
} catch (e) {
return reject(e)
}
}
})
}
const handleGistResponse = (error, data) => {
if (error) {
dispatch(displayNotification('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', null))
} else {
if (data.html_url) {
dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => {
window.open(data.html_url, '_blank')
}, () => {}))
} else {
const error = JSON.stringify(data.errors, null, '\t') || ''
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message
dispatch(displayNotification('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', null))
}
}
}
/**
* This function is to get the original content of given gist
* @params id is the gist id to fetch
*/
const getOriginalFiles = async (id) => {
if (!id) {
return []
}
const url = `https://api.github.com/gists/${id}`
const res = await fetch(url)
const data = await res.json()
return data.files || []
}

@ -0,0 +1,234 @@
import { action } from '../types'
export const setCurrentWorkspace = (workspace: string) => {
return {
type: 'SET_CURRENT_WORKSPACE',
payload: workspace
}
}
export const setWorkspaces = (workspaces: string[]) => {
return {
type: 'SET_WORKSPACES',
payload: workspaces
}
}
export const setMode = (mode: 'browser' | 'localhost') => {
return {
type: 'SET_MODE',
payload: mode
}
}
export const fetchDirectoryError = (error: any) => {
return {
type: 'FETCH_DIRECTORY_ERROR',
payload: error
}
}
export const fetchDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_DIRECTORY_REQUEST',
payload: promise
}
}
export const fetchDirectorySuccess = (path: string, fileTree) => {
return {
type: 'FETCH_DIRECTORY_SUCCESS',
payload: { path, fileTree }
}
}
export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => {
return {
type: 'DISPLAY_NOTIFICATION',
payload: { title, message, labelOk, labelCancel, actionOk, actionCancel }
}
}
export const hideNotification = () => {
return {
type: 'HIDE_NOTIFICATION'
}
}
export const fileAddedSuccess = (filePath: string) => {
return {
type: 'FILE_ADDED_SUCCESS',
payload: filePath
}
}
export const folderAddedSuccess = (path: string, folderPath: string, fileTree) => {
return {
type: 'FOLDER_ADDED_SUCCESS',
payload: { path, folderPath, fileTree }
}
}
export const fileRemovedSuccess = (removePath: string) => {
return {
type: 'FILE_REMOVED_SUCCESS',
payload: removePath
}
}
export const fileRenamedSuccess = (path: string, oldPath: string, fileTree) => {
return {
type: 'FILE_RENAMED_SUCCESS',
payload: { path, oldPath, fileTree }
}
}
export const rootFolderChangedSuccess = (path: string) => {
return {
type: 'ROOT_FOLDER_CHANGED',
payload: path
}
}
export const addInputFieldSuccess = (path: string, fileTree, type: 'file' | 'folder' | 'gist') => {
return {
type: 'ADD_INPUT_FIELD',
payload: { path, fileTree, type }
}
}
export const removeInputFieldSuccess = (path: string) => {
return {
type: 'REMOVE_INPUT_FIELD',
payload: { path }
}
}
export const setReadOnlyMode = (mode: boolean) => {
return {
type: 'SET_READ_ONLY_MODE',
payload: mode
}
}
export const createWorkspaceError = (error: any) => {
return {
type: 'CREATE_WORKSPACE_ERROR',
payload: error
}
}
export const createWorkspaceRequest = (promise: Promise<any>) => {
return {
type: 'CREATE_WORKSPACE_REQUEST',
payload: promise
}
}
export const createWorkspaceSuccess = (workspaceName: string) => {
return {
type: 'CREATE_WORKSPACE_SUCCESS',
payload: workspaceName
}
}
export const fetchWorkspaceDirectoryError = (error: any) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_ERROR',
payload: error
}
}
export const fetchWorkspaceDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_REQUEST',
payload: promise
}
}
export const fetchWorkspaceDirectorySuccess = (path: string, fileTree) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_SUCCESS',
payload: { path, fileTree }
}
}
export const setRenameWorkspace = (oldName: string, workspaceName: string) => {
return {
type: 'RENAME_WORKSPACE',
payload: { oldName, workspaceName }
}
}
export const setDeleteWorkspace = (workspaceName: string) => {
return {
type: 'DELETE_WORKSPACE',
payload: workspaceName
}
}
export const displayPopUp = (message: string) => {
return {
type: 'DISPLAY_POPUP_MESSAGE',
payload: message
}
}
export const hidePopUp = () => {
return {
type: 'HIDE_POPUP_MESSAGE'
}
}
export const focusElement = (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => {
return {
type: 'SET_FOCUS_ELEMENT',
payload: elements
}
}
export const setContextMenuItem = (item: action) => {
return {
type: 'SET_CONTEXT_MENU_ITEM',
payload: item
}
}
export const removeContextMenuItem = (plugin) => {
return {
type: 'REMOVE_CONTEXT_MENU_ITEM',
payload: plugin
}
}
export const setExpandPath = (paths: string[]) => {
return {
type: 'SET_EXPAND_PATH',
payload: paths
}
}
export const loadLocalhostError = (error: any) => {
return {
type: 'LOAD_LOCALHOST_ERROR',
payload: error
}
}
export const loadLocalhostRequest = () => {
return {
type: 'LOAD_LOCALHOST_REQUEST'
}
}
export const loadLocalhostSuccess = () => {
return {
type: 'LOAD_LOCALHOST_SUCCESS'
}
}
export const fsInitializationCompleted = () => {
return {
type: 'FS_INITIALIZATION_COMPLETED'
}
}

@ -0,0 +1,299 @@
import React from 'react'
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import axios, { AxiosResponse } from 'axios'
import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload'
import { checkSlash, checkSpecialChars } from '@remix-ui/helper'
const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples')
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params')
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
const queryParams = new QueryParams()
let plugin, dispatch: React.Dispatch<any>
export const setPlugin = (filePanelPlugin, reducerDispatch) => {
plugin = filePanelPlugin
dispatch = reducerDispatch
}
export const addInputField = async (type: 'file' | 'folder', path: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
const provider = plugin.fileManager.currentFileProvider()
const promise = new Promise((resolve, reject) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) {
cb && cb(error)
return reject(error)
}
cb && cb(null, true)
resolve(fileTree)
})
})
promise.then((files) => {
dispatch(addInputFieldSuccess(path, files, type))
}).catch((error) => {
console.error(error)
})
return promise
}
export const createWorkspace = async (workspaceName: string, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, 'default-template')
dispatch(createWorkspaceRequest(promise))
promise.then(async () => {
dispatch(createWorkspaceSuccess(workspaceName))
plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
plugin.setWorkspaces(await getWorkspaces())
plugin.workspaceCreated(workspaceName)
if (!isEmpty) await loadWorkspacePreset('default-template')
cb && cb(null, workspaceName)
}).catch((error) => {
dispatch(createWorkspaceError({ error }))
cb && cb(error)
})
return promise
}
export const createWorkspaceTemplate = async (workspaceName: string, template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => {
if (!workspaceName) throw new Error('workspace name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await workspaceExists(workspaceName) && template === 'default-template') throw new Error('workspace already exists')
else {
const workspaceProvider = plugin.fileProviders.workspace
await workspaceProvider.createWorkspace(workspaceName)
}
}
export const loadWorkspacePreset = async (template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => {
const workspaceProvider = plugin.fileProviders.workspace
const params = queryParams.get()
switch (template) {
case 'code-template':
// creates a new workspace code-sample and loads code from url params.
try {
let path = ''; let content = ''
if (params.code) {
const hash = bufferToHex(keccakFromString(params.code))
path = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol'
content = atob(params.code)
workspaceProvider.set(path, content)
}
if (params.url) {
const data = await plugin.call('contentImport', 'resolve', params.url)
path = data.cleanUrl
content = data.content
workspaceProvider.set(path, content)
}
return path
} catch (e) {
console.error(e)
}
break
case 'gist-template':
// creates a new workspace gist-sample and get the file from gist
try {
const gistId = params.gist
const response: AxiosResponse = await axios.get(`https://api.github.com/gists/${gistId}`)
const data = response.data as { files: any }
if (!data.files) {
return dispatch(displayNotification('Gist load error', 'No files found', 'OK', null, () => { dispatch(hideNotification()) }, null))
}
const obj = {}
Object.keys(data.files).forEach((element) => {
const path = element.replace(/\.\.\./g, '/')
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element]
})
plugin.fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => {
if (!errorLoadingFile) {
const provider = plugin.fileManager.getProvider('workspace')
provider.lastLoadedGistId = gistId
} else {
dispatch(displayNotification('', errorLoadingFile.message || errorLoadingFile, 'OK', null, () => {}, null))
}
})
} catch (e) {
dispatch(displayNotification('Gist load error', e.message, 'OK', null, () => { dispatch(hideNotification()) }, null))
console.error(e)
}
break
case 'default-template':
// creates a new workspace and populates it with default project template.
// insert example contracts
for (const file in examples) {
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}
break
}
}
export const workspaceExists = async (name: string) => {
const workspaceProvider = plugin.fileProviders.workspace
const browserProvider = plugin.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
return browserProvider.exists(workspacePath)
}
export const fetchWorkspaceDirectory = async (path: string) => {
if (!path) return
const provider = plugin.fileManager.currentFileProvider()
const promise = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
dispatch(fetchWorkspaceDirectoryRequest(promise))
promise.then((fileTree) => {
dispatch(fetchWorkspaceDirectorySuccess(path, fileTree))
}).catch((error) => {
dispatch(fetchWorkspaceDirectoryError({ error }))
})
return promise
}
export const renameWorkspace = async (oldName: string, workspaceName: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
await renameWorkspaceFromProvider(oldName, workspaceName)
await dispatch(setRenameWorkspace(oldName, workspaceName))
plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
plugin.workspaceRenamed(oldName, workspaceName)
cb && cb(null, workspaceName)
}
export const renameWorkspaceFromProvider = async (oldName: string, workspaceName: string) => {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await workspaceExists(workspaceName)) throw new Error('workspace already exists')
const browserProvider = plugin.fileProviders.browser
const workspaceProvider = plugin.fileProviders.workspace
const workspacesPath = workspaceProvider.workspacesPath
browserProvider.rename('browser/' + workspacesPath + '/' + oldName, 'browser/' + workspacesPath + '/' + workspaceName, true)
workspaceProvider.setWorkspace(workspaceName)
plugin.setWorkspaces(await getWorkspaces())
}
export const deleteWorkspace = async (workspaceName: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
await deleteWorkspaceFromProvider(workspaceName)
await dispatch(setDeleteWorkspace(workspaceName))
plugin.workspaceDeleted(workspaceName)
cb && cb(null, workspaceName)
}
const deleteWorkspaceFromProvider = async (workspaceName: string) => {
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
await plugin.fileManager.closeAllFiles()
plugin.fileProviders.browser.remove(workspacesPath + '/' + workspaceName)
plugin.setWorkspaces(await getWorkspaces())
}
export const switchToWorkspace = async (name: string) => {
await plugin.fileManager.closeAllFiles()
if (name === LOCALHOST) {
const isActive = await plugin.call('manager', 'isActive', 'remixd')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'remixd')
dispatch(setMode('localhost'))
plugin.emit('setWorkspace', { name: null, isLocalhost: true })
} else if (name === NO_WORKSPACE) {
plugin.fileProviders.workspace.clearWorkspace()
plugin.setWorkspace({ name: null, isLocalhost: false })
dispatch(setCurrentWorkspace(null))
} else {
const isActive = await plugin.call('manager', 'isActive', 'remixd')
if (isActive) plugin.call('manager', 'deactivatePlugin', 'remixd')
await plugin.fileProviders.workspace.setWorkspace(name)
plugin.setWorkspace({ name, isLocalhost: false })
dispatch(setMode('browser'))
dispatch(setCurrentWorkspace(name))
dispatch(setReadOnlyMode(false))
}
}
export const uploadFile = async (target, targetFolder: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
// TODO The file explorer is merely a view on the current state of
// the files module. Please ask the user here if they want to overwrite
// a file and then just use `files.add`. The file explorer will
// pick that up via the 'fileAdded' event from the files module.
[...target.files].forEach((file) => {
const workspaceProvider = plugin.fileProviders.workspace
const loadFile = (name: string): void => {
const fileReader = new FileReader()
fileReader.onload = async function (event) {
if (checkSpecialChars(file.name)) {
return dispatch(displayNotification('File Upload Failed', 'Special characters are not allowed', 'Close', null, async () => {}))
}
const success = await workspaceProvider.set(name, event.target.result)
if (!success) {
return dispatch(displayNotification('File Upload Failed', 'Failed to create file ' + name, 'Close', null, async () => {}))
}
const config = plugin.registry.get('config').api
const editor = plugin.registry.get('editor').api
if ((config.get('currentFile') === name) && (editor.currentContent() !== event.target.result)) {
editor.setText(event.target.result)
}
}
fileReader.readAsText(file)
cb && cb(null, true)
}
const name = `${targetFolder}/${file.name}`
workspaceProvider.exists(name).then(exist => {
if (!exist) {
loadFile(name)
} else {
dispatch(displayNotification('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', null, () => {
loadFile(name)
}, () => {}))
}
}).catch(error => {
cb && cb(error)
if (error) console.log(error)
})
})
}
export 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) {
return reject(error)
}
resolve(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map((folder) => folder.replace(workspacesPath + '/', '')))
})
})
plugin.setWorkspaces(workspaces)
return workspaces
} catch (e) {}
}

@ -1,7 +1,7 @@
import React, { useRef, useEffect } from 'react' // eslint-disable-line
import { action, FileExplorerContextMenuProps } from './types'
import { action, FileExplorerContextMenuProps } from '../types'
import './css/file-explorer-context-menu.css'
import '../css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
declare global {

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react' //eslint-disable-line
import { FileExplorerMenuProps } from './types'
import { FileExplorerMenuProps } from '../types'
export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
const [state, setState] = useState({

@ -0,0 +1,473 @@
import React, { useEffect, useState, useContext, SyntheticEvent } from 'react' // eslint-disable-line
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, MenuItems, FileExplorerState } from '../types'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
import { contextMenuActions } from '../utils'
import '../css/file-explorer.css'
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileRender } from './file-render'
export const FileExplorer = (props: FileExplorerProps) => {
const { name, contextMenuItems, removedContextMenuItems, files } = props
const [state, setState] = useState<FileExplorerState>({
ctrlKey: false,
newFileName: '',
actions: contextMenuActions,
focusContext: {
element: null,
x: null,
y: null,
type: ''
},
focusEdit: {
element: null,
type: '',
isNew: false,
lastEdit: ''
},
mouseOverElement: null,
showContextMenu: false,
reservedKeywords: [name, 'gist-'],
copyElement: []
})
const [canPaste, setCanPaste] = useState(false)
useEffect(() => {
if (contextMenuItems) {
addMenuItems(contextMenuItems)
}
}, [contextMenuItems])
useEffect(() => {
if (removedContextMenuItems) {
removeMenuItems(removedContextMenuItems)
}
}, [contextMenuItems])
useEffect(() => {
if (props.focusEdit) {
setState(prevState => {
return { ...prevState, focusEdit: { element: props.focusEdit, type: 'file', isNew: true, lastEdit: null } }
})
}
}, [props.focusEdit])
useEffect(() => {
const keyPressHandler = (e: KeyboardEvent) => {
if (e.shiftKey) {
setState(prevState => {
return { ...prevState, ctrlKey: true }
})
}
}
const keyUpHandler = (e: KeyboardEvent) => {
if (!e.shiftKey) {
setState(prevState => {
return { ...prevState, ctrlKey: false }
})
}
}
document.addEventListener('keydown', keyPressHandler)
document.addEventListener('keyup', keyUpHandler)
return () => {
document.removeEventListener('keydown', keyPressHandler)
document.removeEventListener('keyup', keyUpHandler)
}
}, [])
useEffect(() => {
if (canPaste) {
addMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
} else {
removeMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
}
}, [canPaste])
const addMenuItems = (items: MenuItems) => {
setState(prevState => {
// filter duplicate items
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...actions] }
})
}
const removeMenuItems = (items: MenuItems) => {
setState(prevState => {
const actions = prevState.actions.filter(({ id, name }) => items.findIndex(item => id === item.id && name === item.name) === -1)
return { ...prevState, actions }
})
}
const hasReservedKeyword = (content: string): boolean => {
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true
else return false
}
const getFocusedFolder = () => {
if (props.focusElement[0]) {
if (props.focusElement[0].type === 'folder' && props.focusElement[0].key) return props.focusElement[0].key
else if (props.focusElement[0].type === 'gist' && props.focusElement[0].key) return props.focusElement[0].key
else if (props.focusElement[0].type === 'file' && props.focusElement[0].key) return extractParentFromKey(props.focusElement[0].key) ? extractParentFromKey(props.focusElement[0].key) : name
else return name
}
}
const createNewFile = async (newFilePath: string) => {
try {
props.dispatchCreateNewFile(newFilePath, props.name)
} catch (error) {
return props.modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
const createNewFolder = async (newFolderPath: string) => {
try {
props.dispatchCreateNewFolder(newFolderPath, props.name)
} catch (e) {
return props.modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {})
}
}
const deletePath = async (path: string[]) => {
if (props.readonly) return props.toast('cannot delete file. ' + name + ' is a read only explorer')
if (!Array.isArray(path)) path = [path]
props.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { props.dispatchDeletePath(path) }, 'Cancel', () => {})
}
const renamePath = async (oldPath: string, newPath: string) => {
try {
props.dispatchRenamePath(oldPath, newPath)
} catch (error) {
props.modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
const uploadFile = (target) => {
const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...props.expandPath, parentFolder])]
props.dispatchHandleExpandPath(expandPath)
props.dispatchUploadFile(target, parentFolder)
}
const copyFile = (src: string, dest: string) => {
try {
props.dispatchCopyFile(src, dest)
} catch (error) {
props.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {})
}
}
const copyFolder = (src: string, dest: string) => {
try {
props.dispatchCopyFolder(src, dest)
} catch (error) {
props.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {})
}
}
const publishToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const pushChangesToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFolderToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFileToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const toGist = (path?: string, type?: string) => {
props.dispatchPublishToGist(path, type)
}
const runScript = async (path: string) => {
try {
props.dispatchRunScript(path)
} catch (error) {
props.toast('Run script failed')
}
}
const emitContextMenuEvent = (cmd: customAction) => {
try {
props.dispatchEmitContextMenuEvent(cmd)
} catch (error) {
props.toast(error)
}
}
const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => {
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path
if (!state.ctrlKey) {
props.dispatchHandleClickFile(path, type)
} else {
if (props.focusElement.findIndex(item => item.key === path) !== -1) {
const focusElement = props.focusElement.filter(item => item.key !== path)
props.dispatchSetFocusElement(focusElement)
} else {
const nonRootFocus = props.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
props.dispatchSetFocusElement(nonRootFocus)
}
}
}
const handleClickFolder = async (path: string, type: 'folder' | 'file' | 'gist') => {
if (state.ctrlKey) {
if (props.focusElement.findIndex(item => item.key === path) !== -1) {
const focusElement = props.focusElement.filter(item => item.key !== path)
props.dispatchSetFocusElement(focusElement)
} else {
const nonRootFocus = props.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
props.dispatchSetFocusElement(nonRootFocus)
}
} else {
let expandPath = []
if (!props.expandPath.includes(path)) {
expandPath = [...new Set([...props.expandPath, path])]
props.dispatchFetchDirectory(path)
} else {
expandPath = [...new Set(props.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))]
}
props.dispatchSetFocusElement([{ key: path, type }])
props.dispatchHandleExpandPath(expandPath)
}
}
const handleContextMenu = (pageX: number, pageY: number, path: string, content: string, type: string) => {
if (!content) return
setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path }
})
}
const hideContextMenu = () => {
setState(prevState => {
return { ...prevState, focusContext: { element: null, x: 0, y: 0, type: '' }, showContextMenu: false }
})
}
const editModeOn = (path: string, type: string, isNew: boolean = false) => {
if (props.readonly) return props.toast('Cannot write/modify file system in read only mode.')
setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
})
}
const editModeOff = async (content: string) => {
if (typeof content === 'string') content = content.trim()
const parentFolder = extractParentFromKey(state.focusEdit.element)
if (!content || (content.trim() === '')) {
if (state.focusEdit.isNew) {
props.dispatchRemoveInputField(parentFolder)
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
} else {
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
} else {
if (state.focusEdit.lastEdit === content) {
return setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
if (checkSpecialChars(content)) {
props.modal('Validation Error', 'Special characters are not allowed', 'OK', () => {})
} else {
if (state.focusEdit.isNew) {
if (hasReservedKeyword(content)) {
props.dispatchRemoveInputField(parentFolder)
props.modal('Reserved Keyword', `File name contains Remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
props.dispatchRemoveInputField(parentFolder)
}
} else {
if (hasReservedKeyword(content)) {
props.modal('Reserved Keyword', `File name contains Remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
if (state.focusEdit.element) {
const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath)
const newPath = oldPath.replace(oldName, content)
renamePath(oldPath, newPath)
}
}
}
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
}
}
const handleNewFileInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
const expandPath = [...new Set([...props.expandPath, parentFolder])]
await props.dispatchAddInputField(parentFolder, 'file')
props.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'file', true)
}
const handleNewFolderInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder)
const expandPath = [...new Set([...props.expandPath, parentFolder])]
await props.dispatchAddInputField(parentFolder, 'folder')
props.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'folder', true)
}
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file') => {
setState(prevState => {
return { ...prevState, copyElement: [{ key: path, type }] }
})
setCanPaste(true)
props.toast(`Copied to clipboard ${path}`)
}
const handlePasteClick = (dest: string, destType: string) => {
dest = destType === 'file' ? extractParentFromKey(dest) || props.name : dest
state.copyElement.map(({ key, type }) => {
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest)
})
}
const deleteMessage = (path: string[]) => {
return (
<div>
<div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div>
{
path.map((item, i) => (<li key={i}>{item}</li>))
}
</div>
)
}
const handleFileExplorerMenuClick = (e: SyntheticEvent) => {
e.stopPropagation()
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerUploadFileuploadFile') return // we don't want to let propagate the input of type file
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerFileUpload') return // we don't want to let propagate the input of type file
let expandPath = []
if (!props.expandPath.includes(props.name)) {
expandPath = [props.name, ...new Set([...props.expandPath])]
} else {
expandPath = [...new Set(props.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(props.name)))]
}
props.dispatchHandleExpandPath(expandPath)
}
return (
<div>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem"
controlBehaviour={true}
label={
<div onClick={handleFileExplorerMenuClick}>
<FileExplorerMenu
title={''}
menuItems={props.menuItems}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
publishToGist={publishToGist}
uploadFile={uploadFile}
/>
</div>
}
expand={true}>
<div className='pb-2'>
<TreeView id='treeViewMenu'>
{
files[props.name] && Object.keys(files[props.name]).map((key, index) => <FileRender
file={files[props.name][key]}
index={index}
focusContext={state.focusContext}
focusEdit={state.focusEdit}
focusElement={props.focusElement}
ctrlKey={state.ctrlKey}
expandPath={props.expandPath}
editModeOff={editModeOff}
handleClickFile={handleClickFile}
handleClickFolder={handleClickFolder}
handleContextMenu={handleContextMenu}
key={index}
/>)
}
</TreeView>
</div>
</TreeViewItem>
</TreeView>
{ state.showContextMenu &&
<FileExplorerContextMenu
actions={props.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)}
hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
renamePath={editModeOn}
runScript={runScript}
copy={handleCopyClick}
paste={handlePasteClick}
emit={emitContextMenuEvent}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
path={state.focusContext.element}
type={state.focusContext.type}
focus={props.focusElement}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
/>
}
</div>
)
}
export default FileExplorer

@ -0,0 +1,67 @@
// eslint-disable-next-line no-use-before-define
import React, { useEffect, useRef, useState } from 'react'
import { FileType } from '../types'
export interface FileLabelProps {
file: FileType,
focusEdit: {
element: string
type: string
isNew: boolean
lastEdit: string
}
editModeOff: (content: string) => void
}
export const FileLabel = (props: FileLabelProps) => {
const { file, focusEdit, editModeOff } = props
const [isEditable, setIsEditable] = useState<boolean>(false)
const labelRef = useRef(null)
useEffect(() => {
if (focusEdit.element && file.path) {
setIsEditable(focusEdit.element === file.path)
}
}, [file.path, focusEdit])
useEffect(() => {
if (labelRef.current) {
setTimeout(() => {
labelRef.current.focus()
}, 0)
}
}, [isEditable])
const handleEditInput = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.which === 13) {
event.preventDefault()
editModeOff(labelRef.current.innerText)
labelRef.current.innerText = file.name
}
}
const handleEditBlur = (event: React.SyntheticEvent) => {
event.stopPropagation()
editModeOff(labelRef.current.innerText)
labelRef.current.innerText = file.name
}
return (
<div
className='remixui_items d-inline-block w-100'
ref={isEditable ? labelRef : null}
suppressContentEditableWarning={true}
contentEditable={isEditable}
onKeyDown={handleEditInput}
onBlur={handleEditBlur}
>
<span
title={file.path}
className={'remixui_label ' + (file.isDirectory ? 'folder' : 'remixui_leaf')}
data-path={file.path}
>
{ file.name }
</span>
</div>
)
}

@ -0,0 +1,124 @@
// eslint-disable-next-line no-use-before-define
import React, { SyntheticEvent, useEffect, useState } from 'react'
import { FileType } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { TreeView, TreeViewItem } from '@remix-ui/tree-view'
import { getPathIcon } from '@remix-ui/helper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileLabel } from './file-label'
export interface RenderFileProps {
file: FileType,
index: number,
focusEdit: { element: string, type: string, isNew: boolean, lastEdit: string },
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[],
focusContext: { element: string, x: number, y: number, type: string },
ctrlKey: boolean,
expandPath: string[],
editModeOff: (content: string) => void,
handleClickFolder: (path: string, type: string) => void,
handleClickFile: (path: string, type: string) => void,
handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void
}
export const FileRender = (props: RenderFileProps) => {
const [file, setFile] = useState<FileType>({} as FileType)
const [hover, setHover] = useState<boolean>(false)
const [icon, setIcon] = useState<string>('')
useEffect(() => {
if (props.file && props.file.path && props.file.type) {
setFile(props.file)
setIcon(getPathIcon(props.file.path))
}
}, [props.file])
const labelClass = props.focusEdit.element === file.path
? 'bg-light' : props.focusElement.findIndex(item => item.key === file.path) !== -1
? 'bg-secondary' : hover
? 'bg-light border' : (props.focusContext.element === file.path) && (props.focusEdit.element !== file.path)
? 'bg-light border' : ''
const spreadProps = {
onClick: (e) => e.stopPropagation()
}
const handleFolderClick = (event: SyntheticEvent) => {
event.stopPropagation()
if (props.focusEdit.element !== file.path) props.handleClickFolder(file.path, file.type)
}
const handleFileClick = (event: SyntheticEvent) => {
event.stopPropagation()
if (props.focusEdit.element !== file.path) props.handleClickFile(file.path, file.type)
}
const handleContextMenu = (event: PointerEvent) => {
event.preventDefault()
event.stopPropagation()
props.handleContextMenu(event.pageX, event.pageY, file.path, (event.target as HTMLElement).textContent, file.type)
}
const handleMouseOut = (event: SyntheticEvent) => {
event.stopPropagation()
setHover(false)
}
const handleMouseOver = (event: SyntheticEvent) => {
event.stopPropagation()
setHover(true)
}
if (file.isDirectory) {
return (
<TreeViewItem
id={`treeViewItem${file.path}`}
iconX='pr-3 fa fa-folder'
iconY='pr-3 fa fa-folder-open'
key={`${file.path + props.index}`}
label={<FileLabel file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />}
onClick={handleFolderClick}
onContextMenu={handleContextMenu}
labelClass={labelClass}
controlBehaviour={ props.ctrlKey }
expand={props.expandPath.includes(file.path)}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
{
file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{
Object.keys(file.child).map((key, index) => <FileRender
file={file.child[key]}
index={index}
focusContext={props.focusContext}
focusEdit={props.focusEdit}
focusElement={props.focusElement}
ctrlKey={props.ctrlKey}
editModeOff={props.editModeOff}
handleClickFile={props.handleClickFile}
handleClickFolder={props.handleClickFolder}
handleContextMenu={props.handleContextMenu}
expandPath={props.expandPath}
key={index}
/>)
}
</TreeView> : <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }/>
}
</TreeViewItem>
)
} else {
return (
<TreeViewItem
id={`treeViewItem${file.path}`}
key={`treeView${file.path}`}
label={<FileLabel file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />}
onClick={handleFileClick}
onContextMenu={handleContextMenu}
icon={icon}
labelClass={labelClass}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
/>
)
}
}

@ -0,0 +1,32 @@
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type'
import { createContext, SyntheticEvent } from 'react'
import { BrowserState } from '../reducers/workspace'
export const FileSystemContext = createContext<{
fs: BrowserState,
// eslint-disable-next-line no-undef
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
dispatchInitWorkspace:() => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchCreateWorkspace: (workspaceName: string) => Promise<void>,
toast: (toasterMsg: string) => void,
dispatchFetchWorkspaceDirectory: (path: string) => Promise<void>,
dispatchSwitchToWorkspace: (name: string) => Promise<void>,
dispatchRenameWorkspace: (oldName: string, workspaceName: string) => Promise<void>,
dispatchDeleteWorkspace: (workspaceName: string) => Promise<void>,
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>,
dispatchUploadFile: (target?: SyntheticEvent, targetFolder?: string) => Promise<void>,
dispatchCreateNewFile: (path: string, rootDir: string) => Promise<void>,
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => Promise<void>,
dispatchCreateNewFolder: (path: string, rootDir: string) => Promise<void>,
dispatchDeletePath: (path: string[]) => Promise<void>,
dispatchRenamePath: (oldPath: string, newPath: string) => Promise<void>,
dispatchCopyFile: (src: string, dest: string) => Promise<void>,
dispatchCopyFolder: (src: string, dest: string) => Promise<void>,
dispatchRunScript: (path: string) => Promise<void>,
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>,
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>
}>(null)

@ -0,0 +1,230 @@
// eslint-disable-next-line no-use-before-define
import React, { useReducer, useState, useEffect, SyntheticEvent } from 'react'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from '../actions'
import { Modal, WorkspaceProps } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type'
export const FileSystemProvider = (props: WorkspaceProps) => {
const { plugin } = props
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 [focusToaster, setFocusToaster] = useState<string>('')
const [toasters, setToasters] = useState<string[]>([])
const dispatchInitWorkspace = async () => {
await initWorkspace(plugin)(fsDispatch)
}
const dispatchFetchDirectory = async (path: string) => {
await fetchDirectory(path)
}
const dispatchAddInputField = async (path: string, type: 'file' | 'folder') => {
await addInputField(type, path)
}
const dispatchRemoveInputField = async (path: string) => {
await removeInputField(path)
}
const dispatchCreateWorkspace = async (workspaceName: string) => {
await createWorkspace(workspaceName)
}
const dispatchFetchWorkspaceDirectory = async (path: string) => {
await fetchWorkspaceDirectory(path)
}
const dispatchSwitchToWorkspace = async (name: string) => {
await switchToWorkspace(name)
}
const dispatchRenameWorkspace = async (oldName: string, workspaceName: string) => {
await renameWorkspace(oldName, workspaceName)
}
const dispatchDeleteWorkspace = async (workspaceName: string) => {
await deleteWorkspace(workspaceName)
}
const dispatchPublishToGist = async (path?: string, type?: string) => {
await publishToGist(path, type)
}
const dispatchUploadFile = async (target?: SyntheticEvent, targetFolder?: string) => {
await uploadFile(target, targetFolder)
}
const dispatchCreateNewFile = async (path: string, rootDir: string) => {
await createNewFile(path, rootDir)
}
const dispatchSetFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => {
await setFocusElement(elements)
}
const dispatchCreateNewFolder = async (path: string, rootDir: string) => {
await createNewFolder(path, rootDir)
}
const dispatchDeletePath = async (path: string[]) => {
await deletePath(path)
}
const dispatchRenamePath = async (oldPath: string, newPath: string) => {
await renamePath(oldPath, newPath)
}
const dispatchCopyFile = async (src: string, dest: string) => {
await copyFile(src, dest)
}
const dispatchCopyFolder = async (src: string, dest: string) => {
await copyFolder(src, dest)
}
const dispatchRunScript = async (path: string) => {
await runScript(path)
}
const dispatchEmitContextMenuEvent = async (cmd: customAction) => {
await emitContextMenuEvent(cmd)
}
const dispatchHandleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => {
await handleClickFile(path, type)
}
const dispatchHandleExpandPath = async (paths: string[]) => {
await handleExpandPath(paths)
}
useEffect(() => {
dispatchInitWorkspace()
}, [])
useEffect(() => {
if (modals.length > 0) {
setFocusModal(() => {
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
}
return focusModal
})
const modalList = modals.slice()
modalList.shift()
setModals(modalList)
}
}, [modals])
useEffect(() => {
if (toasters.length > 0) {
setFocusToaster(() => {
return toasters[0]
})
const toasterList = toasters.slice()
toasterList.shift()
setToasters(toasterList)
}
}, [toasters])
useEffect(() => {
if (fs.notification.title) {
modal(fs.notification.title, fs.notification.message, fs.notification.labelOk, fs.notification.actionOk, fs.notification.labelCancel, fs.notification.actionCancel)
}
}, [fs.notification])
useEffect(() => {
if (fs.popup) {
toast(fs.popup)
}
}, [fs.popup])
const handleHideModal = () => {
setFocusModal(modal => {
return { ...modal, hide: true, message: null }
})
}
// eslint-disable-next-line no-undef
const modal = (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => {
setModals(modals => {
modals.push({ message, title, okLabel, okFn, cancelLabel, cancelFn })
return [...modals]
})
}
const handleToaster = () => {
setFocusToaster('')
clearPopUp()
}
const toast = (toasterMsg: string) => {
setToasters(messages => {
messages.push(toasterMsg)
return [...messages]
})
}
const value = {
fs,
modal,
toast,
dispatchInitWorkspace,
dispatchFetchDirectory,
dispatchAddInputField,
dispatchRemoveInputField,
dispatchCreateWorkspace,
dispatchFetchWorkspaceDirectory,
dispatchSwitchToWorkspace,
dispatchRenameWorkspace,
dispatchDeleteWorkspace,
dispatchPublishToGist,
dispatchUploadFile,
dispatchCreateNewFile,
dispatchSetFocusElement,
dispatchCreateNewFolder,
dispatchDeletePath,
dispatchRenamePath,
dispatchCopyFile,
dispatchCopyFolder,
dispatchRunScript,
dispatchEmitContextMenuEvent,
dispatchHandleClickFile,
dispatchHandleExpandPath
}
return (
<FileSystemContext.Provider value={value}>
{ fs.initializingFS && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> }
{ !fs.initializingFS && <Workspace /> }
<ModalDialog id='fileSystem' { ...focusModal } handleHide={ handleHideModal } />
<Toaster message={focusToaster} handleHide={handleToaster} />
</FileSystemContext.Provider>
)
}
export default FileSystemProvider

@ -0,0 +1,815 @@
import { extractNameFromKey } from '@remix-ui/helper'
import { action, FileType } from '../types'
import * as _ from 'lodash'
interface Action {
type: string
payload: any
}
export interface BrowserState {
browser: {
currentWorkspace: string,
workspaces: string[],
files: { [x: string]: Record<string, FileType> },
expandPath: string[]
isRequestingDirectory: boolean,
isSuccessfulDirectory: boolean,
isRequestingWorkspace: boolean,
isSuccessfulWorkspace: boolean,
error: string,
contextMenu: {
registeredMenuItems: action[],
removedMenuItems: action[],
error: string
}
},
localhost: {
sharedFolder: string,
files: { [x: string]: Record<string, FileType> },
expandPath: string[],
isRequestingDirectory: boolean,
isSuccessfulDirectory: boolean,
isRequestingLocalhost: boolean,
isSuccessfulLocalhost: boolean,
error: string,
contextMenu: {
registeredMenuItems: action[],
removedMenuItems: action[],
error: string
}
},
mode: 'browser' | 'localhost',
notification: {
title: string,
message: string,
actionOk: () => void,
actionCancel: (() => void) | null,
labelOk: string,
labelCancel: string
},
readonly: boolean,
popup: string,
focusEdit: string,
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[],
initializingFS: boolean
}
export const browserInitialState: BrowserState = {
browser: {
currentWorkspace: '',
workspaces: [],
files: {},
expandPath: [],
isRequestingDirectory: false,
isSuccessfulDirectory: false,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: null,
contextMenu: {
registeredMenuItems: [],
removedMenuItems: [],
error: null
}
},
localhost: {
sharedFolder: '',
files: {},
expandPath: [],
isRequestingDirectory: false,
isSuccessfulDirectory: false,
isRequestingLocalhost: false,
isSuccessfulLocalhost: false,
error: null,
contextMenu: {
registeredMenuItems: [],
removedMenuItems: [],
error: null
}
},
mode: 'browser',
notification: {
title: '',
message: '',
actionOk: () => {},
actionCancel: () => {},
labelOk: '',
labelCancel: ''
},
readonly: false,
popup: '',
focusEdit: '',
focusElement: [],
initializingFS: true
}
export const browserReducer = (state = browserInitialState, action: Action) => {
switch (action.type) {
case 'SET_CURRENT_WORKSPACE': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload,
workspaces: workspaces.filter(workspace => workspace)
}
}
}
case 'SET_WORKSPACES': {
const payload = action.payload as string[]
return {
...state,
browser: {
...state.browser,
workspaces: payload.filter(workspace => workspace)
}
}
}
case 'SET_MODE': {
const payload = action.payload as 'browser' | 'localhost'
return {
...state,
mode: payload
}
}
case 'FETCH_DIRECTORY_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequestingDirectory: state.mode === 'browser',
isSuccessfulDirectory: false,
error: null
},
localhost: {
...state.localhost,
isRequestingDirectory: state.mode === 'localhost',
isSuccessfulDirectory: false,
error: null
}
}
}
case 'FETCH_DIRECTORY_SUCCESS': {
const payload = action.payload as { path: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files,
isRequestingDirectory: false,
isSuccessfulDirectory: true,
error: null
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files,
isRequestingDirectory: false,
isSuccessfulDirectory: true,
error: null
}
}
}
case 'FETCH_DIRECTORY_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequestingDirectory: false,
isSuccessfulDirectory: false,
error: state.mode === 'browser' ? action.payload : null
},
localhost: {
...state.localhost,
isRequestingDirectory: false,
isSuccessfulDirectory: false,
error: state.mode === 'localhost' ? action.payload : null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: state.mode === 'browser',
isSuccessfulWorkspace: false,
error: null
},
localhost: {
...state.localhost,
isRequestingWorkspace: state.mode === 'localhost',
isSuccessfulWorkspace: false,
error: null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_SUCCESS': {
const payload = action.payload as { path: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchWorkspaceDirectoryContent(state, payload) : state.browser.files,
isRequestingWorkspace: false,
isSuccessfulWorkspace: true,
error: null
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchWorkspaceDirectoryContent(state, payload) : state.localhost.files,
isRequestingWorkspace: false,
isSuccessfulWorkspace: true,
error: null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: state.mode === 'browser' ? action.payload : null
},
localhost: {
...state.localhost,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: state.mode === 'localhost' ? action.payload : null
}
}
}
case 'DISPLAY_NOTIFICATION': {
const payload = action.payload as { title: string, message: string, actionOk: () => void, actionCancel: () => void, labelOk: string, labelCancel: string }
return {
...state,
notification: {
title: payload.title,
message: payload.message,
actionOk: payload.actionOk || browserInitialState.notification.actionOk,
actionCancel: payload.actionCancel || browserInitialState.notification.actionCancel,
labelOk: payload.labelOk,
labelCancel: payload.labelCancel
}
}
}
case 'HIDE_NOTIFICATION': {
return {
...state,
notification: browserInitialState.notification
}
}
case 'FILE_ADDED_SUCCESS': {
const payload = action.payload as string
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fileAdded(state, payload) : state.browser.files,
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload])] : state.browser.expandPath
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fileAdded(state, payload) : state.localhost.files,
expandPath: state.mode === 'localhost' ? [...new Set([...state.localhost.expandPath, payload])] : state.localhost.expandPath
}
}
}
case 'FOLDER_ADDED_SUCCESS': {
const payload = action.payload as { path: string, folderPath: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files,
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload.folderPath])] : state.browser.expandPath
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files,
expandPath: state.mode === 'localhost' ? [...new Set([...state.localhost.expandPath, payload.folderPath])] : state.localhost.expandPath
}
}
}
case 'FILE_REMOVED_SUCCESS': {
const payload = action.payload as string
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fileRemoved(state, payload) : state.browser.files,
expandPath: state.mode === 'browser' ? [...(state.browser.expandPath.filter(path => path !== payload))] : state.browser.expandPath
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fileRemoved(state, payload) : state.localhost.files,
expandPath: state.mode === 'localhost' ? [...(state.browser.expandPath.filter(path => path !== payload))] : state.localhost.expandPath
}
}
}
case 'ROOT_FOLDER_CHANGED': {
const payload = action.payload as string
return {
...state,
localhost: {
...state.localhost,
sharedFolder: payload
}
}
}
case 'ADD_INPUT_FIELD': {
const payload = action.payload as { path: string, fileTree, type: 'file' | 'folder' }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files
},
focusEdit: payload.path + '/' + 'blank'
}
}
case 'REMOVE_INPUT_FIELD': {
const payload = action.payload as { path: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? removeInputField(state, payload.path) : state.browser.files
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? removeInputField(state, payload.path) : state.localhost.files
},
focusEdit: null
}
}
case 'SET_READ_ONLY_MODE': {
const payload = action.payload as boolean
return {
...state,
readonly: payload
}
}
case 'FILE_RENAMED_SUCCESS': {
const payload = action.payload as { path: string, oldPath: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload, payload.oldPath) : state.browser.files
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload, payload.oldPath) : state.localhost.files
}
}
}
case 'CREATE_WORKSPACE_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: true,
isSuccessfulWorkspace: false,
error: null
}
}
}
case 'CREATE_WORKSPACE_SUCCESS': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload,
workspaces: workspaces.filter(workspace => workspace),
isRequestingWorkspace: false,
isSuccessfulWorkspace: true,
error: null
}
}
}
case 'CREATE_WORKSPACE_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: action.payload
}
}
}
case 'RENAME_WORKSPACE': {
const payload = action.payload as { oldName: string, workspaceName: string }
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload.oldName))
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload.workspaceName,
workspaces: [...workspaces, payload.workspaceName],
expandPath: []
}
}
}
case 'DELETE_WORKSPACE': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload))
return {
...state,
browser: {
...state.browser,
workspaces: workspaces
}
}
}
case 'DISPLAY_POPUP_MESSAGE': {
const payload = action.payload as string
return {
...state,
popup: payload
}
}
case 'HIDE_POPUP_MESSAGE': {
return {
...state,
popup: ''
}
}
case 'SET_FOCUS_ELEMENT': {
const payload = action.payload as { key: string, type: 'file' | 'folder' | 'gist' }[]
return {
...state,
focusElement: payload
}
}
case 'SET_CONTEXT_MENU_ITEM': {
const payload = action.payload as action
return {
...state,
browser: {
...state.browser,
contextMenu: state.mode === 'browser' ? addContextMenuItem(state, payload) : state.browser.contextMenu
},
localhost: {
...state.localhost,
contextMenu: state.mode === 'localhost' ? addContextMenuItem(state, payload) : state.localhost.contextMenu
}
}
}
case 'REMOVE_CONTEXT_MENU_ITEM': {
const payload = action.payload
return {
...state,
browser: {
...state.browser,
contextMenu: state.mode === 'browser' ? removeContextMenuItem(state, payload) : state.browser.contextMenu
},
localhost: {
...state.localhost,
contextMenu: state.mode === 'localhost' ? removeContextMenuItem(state, payload) : state.localhost.contextMenu
}
}
}
case 'SET_EXPAND_PATH': {
const payload = action.payload as string[]
return {
...state,
browser: {
...state.browser,
expandPath: state.mode === 'browser' ? payload : state.browser.expandPath
},
localhost: {
...state.localhost,
expandPath: state.mode === 'localhost' ? payload : state.localhost.expandPath
}
}
}
case 'LOAD_LOCALHOST_REQUEST': {
return {
...state,
localhost: {
...state.localhost,
isRequestingLocalhost: true,
isSuccessfulLocalhost: false,
error: null
}
}
}
case 'LOAD_LOCALHOST_SUCCESS': {
return {
...state,
localhost: {
...state.localhost,
isRequestingLocalhost: false,
isSuccessfulLocalhost: true,
error: null
}
}
}
case 'LOAD_LOCALHOST_ERROR': {
const payload = action.payload as string
return {
...state,
localhost: {
...state.localhost,
isRequestingLocalhost: false,
isSuccessfulLocalhost: false,
error: payload
}
}
}
case 'FS_INITIALIZATION_COMPLETED': {
return {
...state,
initializingFS: false
}
}
default:
throw new Error()
}
}
const fileAdded = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => {
let files = state.mode === 'browser' ? state.browser.files : state.localhost.files
const _path = splitPath(state, path)
files = _.set(files, _path, {
path: path,
name: extractNameFromKey(path),
isDirectory: false,
type: 'file'
})
return files
}
const fileRemoved = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => {
const files = state.mode === 'browser' ? state.browser.files : state.localhost.files
const _path = splitPath(state, path)
_.unset(files, _path)
return files
}
const removeInputField = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => {
let files = state.mode === 'browser' ? state.browser.files : state.localhost.files
const root = state.mode === 'browser' ? state.browser.currentWorkspace : state.mode
if (path === root) {
delete files[root][path + '/' + 'blank']
return files
}
const _path = splitPath(state, path)
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child && prevFiles.child[path + '/' + 'blank'] && delete prevFiles.child[path + '/' + 'blank']
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: prevFiles ? prevFiles.child : {}
})
}
return files
}
// IDEA: Modify function to remove blank input field without fetching content
const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: string, type?: 'file' | 'folder' }, deletePath?: string): { [x: string]: Record<string, FileType> } => {
if (!payload.fileTree) return state.mode === 'browser' ? state.browser.files : state[state.mode].files
if (state.mode === 'browser') {
if (payload.path === state.browser.currentWorkspace) {
let files = normalize(payload.fileTree, payload.path, payload.type)
files = _.merge(files, state.browser.files[state.browser.currentWorkspace])
if (deletePath) delete files[deletePath]
return { [state.browser.currentWorkspace]: files }
} else {
let files = state.browser.files
const _path = splitPath(state, payload.path)
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) {
if (deletePath.endsWith('/blank')) delete prevFiles.child[deletePath]
else {
deletePath = extractNameFromKey(deletePath)
delete prevFiles.child[deletePath]
}
}
files = _.set(files, _path, prevFiles)
} else if (payload.fileTree && payload.path) {
files = { [payload.path]: normalize(payload.fileTree, payload.path, payload.type) }
}
return files
}
} else {
if (payload.path === state.mode || payload.path === '/') {
let files = normalize(payload.fileTree, payload.path, payload.type)
files = _.merge(files, state[state.mode].files[state.mode])
if (deletePath) delete files[deletePath]
return { [state.mode]: files }
} else {
let files = state.localhost.files
const _path = splitPath(state, payload.path)
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) {
if (deletePath.endsWith('/blank')) delete prevFiles.child[deletePath]
else {
deletePath = extractNameFromKey(deletePath)
delete prevFiles.child[deletePath]
}
}
files = _.set(files, _path, prevFiles)
} else {
files = { [payload.path]: normalize(payload.fileTree, payload.path, payload.type) }
}
return files
}
}
}
const fetchWorkspaceDirectoryContent = (state: BrowserState, payload: { fileTree, path: string }): { [x: string]: Record<string, FileType> } => {
if (state.mode === 'browser') {
const files = normalize(payload.fileTree, payload.path)
return { [payload.path]: files }
} else {
return fetchDirectoryContent(state, payload)
}
}
const normalize = (filesList, directory?: string, newInputType?: 'folder' | 'file'): Record<string, FileType> => {
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 = directory + '/blank'
folders[path] = {
path: path,
name: '',
isDirectory: true,
type: 'folder'
}
} else if (newInputType === 'file') {
const path = directory + '/blank'
files[path] = {
path: path,
name: '',
isDirectory: false,
type: 'file'
}
}
return Object.assign({}, folders, files)
}
const splitPath = (state: BrowserState, path: string): string[] | string => {
const root = state.mode === 'browser' ? state.browser.currentWorkspace : 'localhost'
const pathArr: string[] = (path || '').split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
return _path
}
const addContextMenuItem = (state: BrowserState, item: action): { registeredMenuItems: action[], removedMenuItems: action[], error: string } => {
let registeredItems = state[state.mode].contextMenu.registeredMenuItems
let removedItems = state[state.mode].contextMenu.removedMenuItems
let error = null
if (registeredItems.filter((o) => {
return o.id === item.id && o.name === item.name
}).length) {
error = `Action ${item.name} already exists on ${item.id}`
return {
registeredMenuItems: registeredItems,
removedMenuItems: removedItems,
error
}
}
registeredItems = [...registeredItems, item]
removedItems = removedItems.filter(menuItem => item.id !== menuItem.id)
return {
registeredMenuItems: registeredItems,
removedMenuItems: removedItems,
error
}
}
const removeContextMenuItem = (state: BrowserState, plugin): { registeredMenuItems: action[], removedMenuItems: action[], error: string } => {
let registeredItems = state[state.mode].contextMenu.registeredMenuItems
const removedItems = state[state.mode].contextMenu.removedMenuItems
const error = null
registeredItems = registeredItems.filter((item) => {
if (item.id !== plugin.name || item.sticky === true) return true
else {
removedItems.push(item)
return false
}
})
return {
registeredMenuItems: registeredItems,
removedMenuItems: removedItems,
error
}
}

@ -1,212 +1,64 @@
import React, { useState, useEffect, useRef } 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 { MenuItems } from 'libs/remix-ui/file-explorer/src/lib/types'
import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line
import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import './css/remix-ui-workspace.css'
import { FileSystemContext } from './contexts'
/* eslint-disable-next-line */
export interface WorkspaceProps {
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void,
createWorkspace: (name: string) => void,
renameWorkspace: (oldName: string, newName: string) => void
workspaceRenamed: ({ name: string }) => void,
workspaceCreated: ({ name: string }) => void,
workspaceDeleted: ({ name: string }) => void,
workspace: any // workspace provider,
browser: any // browser provider
localhost: any // localhost provider
fileManager : any
registry: any // registry
plugin: any // plugin call and resetFocus
request: any // api request,
workspaces: any,
registeredMenuItems: MenuItems // menu items
removedMenuItems: MenuItems
initialWorkspace: string
}
const canUpload = window.File || window.FileReader || window.FileList || window.Blob
var canUpload = window.File || window.FileReader || window.FileList || window.Blob
export const Workspace = (props: WorkspaceProps) => {
export function Workspace () {
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
/* extends the parent 'plugin' with some function needed by the file explorer */
props.plugin.resetFocus = (reset) => {
setState(prevState => {
return { ...prevState, reset }
})
}
props.plugin.resetNewFile = () => {
setState(prevState => {
return { ...prevState, displayNewFile: !state.displayNewFile }
})
}
props.plugin.resetUploadFile = () => {}
/* implement an external API, consumed by the parent */
props.request.createWorkspace = () => {
return createWorkspace()
}
props.request.setWorkspace = (workspaceName) => {
return setWorkspace(workspaceName)
}
props.request.createNewFile = async () => {
if (!state.workspaces.length) await createNewWorkspace('default_workspace')
props.plugin.resetNewFile()
}
props.request.uploadFile = async (target) => {
if (!state.workspaces.length) await createNewWorkspace('default_workspace')
setState(prevState => {
return { ...prevState, uploadFileEvent: target }
})
}
props.request.getCurrentWorkspace = () => {
return { name: state.currentWorkspace, isLocalhost: state.currentWorkspace === LOCALHOST, absolutePath: `${props.workspace.workspacesPath}/${state.currentWorkspace}` }
}
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const global = useContext(FileSystemContext)
const workspaceRenameInput = useRef()
const workspaceCreateInput = useRef()
useEffect(() => {
let getWorkspaces = async () => {
if (props.workspaces && Array.isArray(props.workspaces)) {
if (props.workspaces.length > 0 && state.currentWorkspace === NO_WORKSPACE) {
const currentWorkspace = props.workspace.getWorkspace() ? props.workspace.getWorkspace() : props.workspaces[0]
await props.workspace.setWorkspace(currentWorkspace)
setState(prevState => {
return { ...prevState, workspaces: props.workspaces, currentWorkspace }
})
} else {
setState(prevState => {
return { ...prevState, workspaces: props.workspaces }
})
}
}
}
getWorkspaces()
return () => {
getWorkspaces = async () => {}
}
}, [props.workspaces])
const localhostDisconnect = () => {
if (state.currentWorkspace === LOCALHOST) setWorkspace(props.workspaces.length > 0 ? props.workspaces[0] : NO_WORKSPACE)
// This should be removed some time after refactoring: https://github.com/ethereum/remix-project/issues/1197
else {
setWorkspace(state.currentWorkspace) // Useful to switch to last selcted workspace when remixd is disconnected
props.fileManager.setMode('browser')
}
}
resetFocus()
}, [])
useEffect(() => {
props.localhost.event.off('disconnected', localhostDisconnect)
props.localhost.event.on('disconnected', localhostDisconnect)
props.localhost.event.on('connected', () => {
remixdExplorer.show()
setWorkspace(LOCALHOST)
})
props.localhost.event.on('disconnected', () => {
remixdExplorer.hide()
})
props.localhost.event.on('loading', () => {
remixdExplorer.loading()
})
props.workspace.event.on('createWorkspace', (name) => {
createNewWorkspace(name)
})
if (props.initialWorkspace) {
props.workspace.setWorkspace(props.initialWorkspace)
setState(prevState => {
return { ...prevState, currentWorkspace: props.initialWorkspace }
})
if (global.fs.mode === 'browser') {
if (global.fs.browser.currentWorkspace) setCurrentWorkspace(global.fs.browser.currentWorkspace)
else setCurrentWorkspace(NO_WORKSPACE)
global.dispatchFetchWorkspaceDirectory(global.fs.browser.currentWorkspace)
} else if (global.fs.mode === 'localhost') {
// global.dispatchFetchWorkspaceDirectory('/')
setCurrentWorkspace(LOCALHOST)
}
}, [])
}, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode])
const createNewWorkspace = async (workspaceName) => {
try {
await props.fileManager.closeAllFiles()
await props.createWorkspace(workspaceName)
await setWorkspace(workspaceName)
toast('New default workspace has been created.')
} catch (e) {
modalMessage('Create Default Workspace', e.message)
console.error(e)
useEffect(() => {
if (global.fs.browser.currentWorkspace && !global.fs.browser.workspaces.includes(global.fs.browser.currentWorkspace)) {
if (global.fs.browser.workspaces.length > 0) {
switchWorkspace(global.fs.browser.workspaces[global.fs.browser.workspaces.length - 1])
} else {
switchWorkspace(NO_WORKSPACE)
}
}
}
const [state, setState] = useState({
workspaces: [],
reset: false,
currentWorkspace: NO_WORKSPACE,
hideRemixdExplorer: true,
displayNewFile: false,
externalUploads: null,
uploadFileEvent: null,
modal: {
hide: true,
title: '',
message: null,
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {},
handleHide: null
},
loadingLocalhost: false,
toasterMsg: ''
})
const toast = (message: string) => {
setState(prevState => {
return { ...prevState, toasterMsg: message }
})
}
/* workspace creation, renaming and deletion */
}, [global.fs.browser.workspaces])
const renameCurrentWorkspace = () => {
modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '', () => {})
global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '')
}
const createWorkspace = () => {
modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '', () => {})
global.modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '')
}
const deleteCurrentWorkspace = () => {
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
modal(title, body, 'OK', () => {}, '', null)
}, 200)
global.modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '')
}
const workspaceRenameInput = useRef()
const workspaceCreateInput = useRef()
const onFinishRenameWorkspace = async () => {
if (workspaceRenameInput.current === undefined) return
// @ts-ignore: Object is possibly 'null'.
const workspaceName = workspaceRenameInput.current.value
try {
await props.renameWorkspace(state.currentWorkspace, workspaceName)
setWorkspace(workspaceName)
props.workspaceRenamed({ name: workspaceName })
await global.dispatchRenameWorkspace(currentWorkspace, workspaceName)
} catch (e) {
modalMessage('Rename Workspace', e.message)
global.modal('Rename Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
}
@ -217,105 +69,40 @@ export const Workspace = (props: WorkspaceProps) => {
const workspaceName = workspaceCreateInput.current.value
try {
await props.fileManager.closeAllFiles()
await props.createWorkspace(workspaceName)
await setWorkspace(workspaceName)
await global.dispatchCreateWorkspace(workspaceName)
} catch (e) {
modalMessage('Create Workspace', e.message)
global.modal('Create Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
}
const onFinishDeleteWorkspace = async () => {
await props.fileManager.closeAllFiles()
const workspacesPath = props.workspace.workspacesPath
props.browser.remove(workspacesPath + '/' + state.currentWorkspace)
const name = state.currentWorkspace
setWorkspace(NO_WORKSPACE)
props.workspaceDeleted({ name })
try {
await global.dispatchDeleteWorkspace(global.fs.browser.currentWorkspace)
} catch (e) {
global.modal('Delete Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
}
/** ** ****/
const resetFocus = (reset) => {
setState(prevState => {
return { ...prevState, reset }
})
const resetFocus = () => {
global.dispatchSetFocusElement([{ key: '', type: 'folder' }])
}
const setWorkspace = async (name) => {
await props.fileManager.closeAllFiles()
if (name === LOCALHOST) {
props.workspace.clearWorkspace()
} else if (name === NO_WORKSPACE) {
props.workspace.clearWorkspace()
} else {
await props.workspace.setWorkspace(name)
}
await props.setWorkspace({ name, isLocalhost: name === LOCALHOST }, !(name === LOCALHOST || name === NO_WORKSPACE))
props.plugin.getWorkspaces()
setState(prevState => {
return { ...prevState, currentWorkspace: name }
})
}
const remixdExplorer = {
hide: async () => {
// If 'connect to localhost' is clicked from home tab, mode is not 'localhost'
// if (props.fileManager.mode === 'localhost') {
await setWorkspace(NO_WORKSPACE)
props.fileManager.setMode('browser')
setState(prevState => {
return { ...prevState, hideRemixdExplorer: true, loadingLocalhost: false }
})
// } else {
// // Hide spinner in file explorer
// setState(prevState => {
// return { ...prevState, loadingLocalhost: false }
// })
// }
},
show: () => {
props.fileManager.setMode('localhost')
setState(prevState => {
return { ...prevState, hideRemixdExplorer: false, loadingLocalhost: false }
})
},
loading: () => {
setState(prevState => {
return { ...prevState, loadingLocalhost: true }
})
const switchWorkspace = async (name: string) => {
try {
await global.dispatchSwitchToWorkspace(name)
global.dispatchHandleExpandPath([])
} catch (e) {
global.modal('Switch To Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
}
const handleHideModal = () => {
setState(prevState => {
return { ...prevState, modal: { ...state.modal, hide: true, message: null } }
})
}
// eslint-disable-next-line no-undef
const modal = async (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel: string, cancelFn: () => void) => {
await setState(prevState => {
return {
...prevState,
modal: {
...prevState.modal,
hide: false,
message,
title,
okLabel,
okFn,
cancelLabel,
cancelFn,
handleHide: handleHideModal
}
}
})
}
const createModalMessage = () => {
return (
<>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" />
</>
)
@ -324,29 +111,14 @@ export const Workspace = (props: WorkspaceProps) => {
const renameModalMessage = () => {
return (
<>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ state.currentWorkspace } ref={workspaceRenameInput} className="form-control" />
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ currentWorkspace } ref={workspaceRenameInput} className="form-control" />
</>
)
}
return (
<div className='remixui_container'>
{ state.modal.message && <ModalDialog
id='workspacesModalDialog'
title={ state.modal.title }
message={ state.modal.message }
hide={ state.modal.hide }
okLabel={ state.modal.okLabel }
okFn={ state.modal.okFn }
cancelLabel={ state.modal.cancelLabel }
cancelFn={ state.modal.cancelFn }
handleHide={ handleHideModal }>
{ (typeof state.modal.message !== 'string') && state.modal.message }
</ModalDialog>
}
<Toaster message={state.toasterMsg} />
<div className='remixui_fileexplorer' onClick={() => resetFocus(true)}>
<div className='remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div>
<header>
<div className="mb-2">
@ -365,7 +137,7 @@ export const Workspace = (props: WorkspaceProps) => {
title='Create'>
</span>
<span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE}
hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceRename'
data-id='workspaceRename'
onClick={(e) => {
@ -376,7 +148,7 @@ export const Workspace = (props: WorkspaceProps) => {
title='Rename'>
</span>
<span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE}
hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceDelete'
data-id='workspaceDelete'
onClick={(e) => {
@ -387,15 +159,15 @@ export const Workspace = (props: WorkspaceProps) => {
title='Delete'>
</span>
</span>
<select id="workspacesSelect" value={state.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) => switchWorkspace(e.target.value)} className="form-control custom-select">
{
state.workspaces
global.fs.browser.workspaces
.map((folder, index) => {
return <option key={index} value={folder}>{folder}</option>
})
}
<option value={LOCALHOST}>{state.currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option>
{ state.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
<option value={LOCALHOST}>{currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option>
{ global.fs.browser.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
</select>
</div>
</header>
@ -403,34 +175,70 @@ export const Workspace = (props: WorkspaceProps) => {
<div className='remixui_fileExplorerTree'>
<div>
<div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'>
{ state.hideRemixdExplorer && state.currentWorkspace && state.currentWorkspace !== NO_WORKSPACE && state.currentWorkspace !== LOCALHOST &&
{ (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<FileExplorer
name={state.currentWorkspace}
registry={props.registry}
filesProvider={props.workspace}
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
removedContextMenuItems={props.removedMenuItems}
displayInput={state.displayNewFile}
externalUploads={state.uploadFileEvent}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
expandPath={global.fs.browser.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
/>
}
</div>
{
state.loadingLocalhost ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>
global.fs.localhost.isRequestingLocalhost ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>
: <div className='pl-2 filesystemexplorer remixui_treeview'>
{ !state.hideRemixdExplorer &&
{ global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost &&
<FileExplorer
name='localhost'
registry={props.registry}
filesProvider={props.localhost}
menuItems={['createNewFile', 'createNewFolder']}
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
removedContextMenuItems={props.removedMenuItems}
contextMenuItems={global.fs.localhost.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
files={global.fs.localhost.files}
expandPath={global.fs.localhost.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
/>
}
</div>

@ -0,0 +1,155 @@
import React from 'react'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
export type action = { name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean }
export type MenuItems = action[]
export interface WorkspaceProps {
plugin: {
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void,
createWorkspace: (name: string) => void,
renameWorkspace: (oldName: string, newName: string) => void
workspaceRenamed: ({ name: string }) => void,
workspaceCreated: ({ name: string }) => void,
workspaceDeleted: ({ name: string }) => void,
workspace: any // workspace provider,
browser: any // browser provider
localhost: any // localhost provider
fileManager : any
registry: any // registry
request: {
createWorkspace: () => void,
setWorkspace: (workspaceName: string) => void,
createNewFile: () => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void,
getCurrentWorkspace: () => void
} // api request,
workspaces: any,
registeredMenuItems: MenuItems // menu items
removedMenuItems: MenuItems
initialWorkspace: string,
resetNewFile: () => void,
getWorkspaces: () => string[]
}
}
export interface WorkspaceState {
hideRemixdExplorer: boolean
displayNewFile: boolean
loadingLocalhost: boolean
}
export interface Modal {
hide?: boolean
title: string
// eslint-disable-next-line no-undef
message: string | JSX.Element
okLabel: string
okFn: () => void
cancelLabel: string
cancelFn: () => void
}
export interface FileType {
path: string,
name: string,
isDirectory: boolean,
type: 'folder' | 'file' | 'gist',
child?: File[]
}
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
menuItems?: string[],
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
files: { [x: string]: Record<string, FileType> },
expandPath: string[],
focusEdit: string,
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[],
dispatchCreateNewFile: (path: string, rootDir: string) => Promise<void>,
// eslint-disable-next-line no-undef
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
dispatchCreateNewFolder: (path: string, rootDir: string) => Promise<void>,
readonly: boolean,
toast: (toasterMsg: string) => void,
dispatchDeletePath: (path: string[]) => Promise<void>,
dispatchRenamePath: (oldPath: string, newPath: string) => Promise<void>,
dispatchUploadFile: (target?: React.SyntheticEvent, targetFolder?: string) => Promise<void>,
dispatchCopyFile: (src: string, dest: string) => Promise<void>,
dispatchCopyFolder: (src: string, dest: string) => Promise<void>,
dispatchRunScript: (path: string) => Promise<void>,
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>,
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>,
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>,
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: string[]) => Promise<void>
}
export interface FileExplorerMenuProps {
title: string,
menuItems: string[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
publishToGist: (path?: string) => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void
}
export interface FileExplorerContextMenuProps {
actions: action[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => void,
hideContextMenu: () => void,
publishToGist?: (path?: string, type?: string) => void,
pushChangesToGist?: (path?: string, type?: string) => void,
publishFolderToGist?: (path?: string, type?: string) => void,
publishFileToGist?: (path?: string, type?: string) => void,
runScript?: (path: string) => void,
emit?: (cmd: customAction) => void,
pageX: number,
pageY: number,
path: string,
type: string,
focus: {key:string, type:string}[],
onMouseOver?: (...args) => void,
copy?: (path: string, type: string) => void,
paste?: (destination: string, type: string) => void
}
export interface FileExplorerState {
ctrlKey: boolean
newFileName: string
actions: {
id: string
name: string
type?: Array<'folder' | 'gist' | 'file'>
path?: string[]
extension?: string[]
pattern?: string[]
multiselect: boolean
label: string
}[]
focusContext: {
element: string
x: number
y: number
type: string
}
focusEdit: {
element: string
type: string
isNew: boolean
lastEdit: string
}
mouseOverElement: string
showContextMenu: boolean
reservedKeywords: string[]
copyElement: {
key: string
type: 'folder' | 'gist' | 'file'
}[]
}

@ -0,0 +1,63 @@
import { MenuItems } from '../types'
export const contextMenuActions: MenuItems = [{
id: 'newFile',
name: 'New File',
type: ['folder', 'gist'],
multiselect: false,
label: ''
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder', 'gist'],
multiselect: false,
label: ''
}, {
id: 'rename',
name: 'Rename',
type: ['file', 'folder'],
multiselect: false,
label: ''
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder', 'gist'],
multiselect: false,
label: ''
}, {
id: 'run',
name: 'Run',
extension: ['.js'],
multiselect: false,
label: ''
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: ['gist'],
multiselect: false,
label: ''
}, {
id: 'publishFolderToGist',
name: 'Publish folder to gist',
type: ['folder'],
multiselect: false,
label: ''
}, {
id: 'publishFileToGist',
name: 'Publish file to gist',
type: ['file'],
multiselect: false,
label: ''
}, {
id: 'copy',
name: 'Copy',
type: ['folder', 'file'],
multiselect: false,
label: ''
}, {
id: 'deleteAll',
name: 'Delete All',
type: ['folder', 'file'],
multiselect: true,
label: ''
}]

@ -148,7 +148,7 @@ function errorHandler (error: any, service: string) {
const gistUrl = 'https://gist.githubusercontent.com/EthereumRemix/091ccc57986452bbb33f57abfb13d173/raw/3367e019335746b73288e3710af2922d4c8ef5a3/origins.json'
try {
const { data } = await Axios.get(gistUrl)
const { data } = (await Axios.get(gistUrl)) as { data: any }
try {
await writeJSON(path.resolve(path.join(__dirname, '..', 'origins.json')), { data })

@ -23,7 +23,7 @@ export class RemixdClient extends PluginClient {
sharedFolder (currentSharedFolder: string): void {
this.currentSharedFolder = currentSharedFolder
if (this.isLoaded) this.emit('rootFolderChanged')
if (this.isLoaded) this.emit('rootFolderChanged', this.currentSharedFolder)
}
list (): Filelist {

@ -88,9 +88,6 @@
"remix-ui-toaster": {
"tags": []
},
"remix-ui-file-explorer": {
"tags": []
},
"debugger": {
"tags": []
},
@ -132,6 +129,9 @@
},
"remix-ui-editor": {
"tags": []
},
"remix-ui-helper": {
"tags": []
}
},
"targetDependencies": {

7215
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor",
"build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"publish:libs": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs",
@ -224,6 +224,7 @@
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.0",
"@types/request": "^2.48.7",
"@types/tape": "^4.13.0",
"@types/ws": "^7.2.4",
"@typescript-eslint/eslint-plugin": "^4.32.0",

@ -45,12 +45,17 @@
"@remix-ui/checkbox": ["libs/remix-ui/checkbox/src/index.ts"],
"@remix-ui/settings": ["libs/remix-ui/settings/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/publish-to-storage": ["libs/remix-ui/publish-to-storage/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"]

@ -623,25 +623,6 @@
}
}
},
"remix-ui-file-explorer": {
"root": "libs/remix-ui/file-explorer",
"sourceRoot": "libs/remix-ui/file-explorer/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/file-explorer/tsconfig.lib.json"],
"exclude": [
"**/node_modules/**",
"!libs/remix-ui/file-explorer/**/*"
]
}
}
}
},
"debugger": {
"root": "apps/debugger",
"sourceRoot": "apps/debugger/src",
@ -1051,6 +1032,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