Merge pull request #924 from ethereum/open-save-workspace

Move workspace to a react component
pull/928/head^2
yann300 4 years ago committed by GitHub
commit f816a5a6bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      apps/remix-ide-e2e/src/commands/removeFile.ts
  2. 12
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  3. 4
      apps/remix-ide-e2e/src/tests/gist.test.ts
  4. 2
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  5. 2
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  6. 18
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  7. 2
      apps/remix-ide-e2e/src/types/index.d.ts
  8. 4
      apps/remix-ide/src/app.js
  9. 10
      apps/remix-ide/src/app/files/fileManager.js
  10. 6
      apps/remix-ide/src/app/files/remixDProvider.js
  11. 8
      apps/remix-ide/src/app/files/remixd-handle.js
  12. 373
      apps/remix-ide/src/app/panels/file-panel.js
  13. 59
      apps/remix-ide/src/app/panels/styles/file-panel-styles.css
  14. 12
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  15. 4
      libs/remix-ui/workspace/.babelrc
  16. 19
      libs/remix-ui/workspace/.eslintrc
  17. 7
      libs/remix-ui/workspace/README.md
  18. 1
      libs/remix-ui/workspace/src/index.ts
  19. 40
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.css
  20. 399
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  21. 16
      libs/remix-ui/workspace/tsconfig.json
  22. 13
      libs/remix-ui/workspace/tsconfig.lib.json
  23. 3
      nx.json
  24. 7
      tsconfig.json
  25. 22
      workspace.json

@ -3,9 +3,9 @@ import { NightwatchBrowser } from 'nightwatch'
const EventEmitter = require('events')
class RemoveFile extends EventEmitter {
command (this: NightwatchBrowser, path: string): NightwatchBrowser {
command (this: NightwatchBrowser, path: string, workspace: string): NightwatchBrowser {
this.api.perform((done) => {
removeFile(this.api, path, () => {
removeFile(this.api, path, workspace, () => {
done()
this.emit('complete')
})
@ -14,7 +14,7 @@ class RemoveFile extends EventEmitter {
}
}
function removeFile (browser: NightwatchBrowser, path: string, done: VoidFunction) {
function removeFile (browser: NightwatchBrowser, path: string, workspace: string, done: VoidFunction) {
browser.execute(function (path) {
function contextMenuClick (element) {
const evt = element.ownerDocument.createEvent('MouseEvents')
@ -39,8 +39,8 @@ function removeFile (browser: NightwatchBrowser, path: string, done: VoidFunctio
.pause(2000)
.perform(() => {
console.log(path, 'to remove')
browser.waitForElementVisible('.modal-ok')
.click('.modal-ok')
browser.waitForElementVisible('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok')
.click('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok')
.waitForElementNotPresent('[data-path="' + path + '"]')
done()
})

@ -40,11 +40,7 @@ module.exports = {
'Should delete file `5_Renamed_Contract.sol` from file explorer': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="treeViewLitreeViewItem5_Renamed_Contract.sol"]')
.rightClick('[data-path="5_Renamed_Contract.sol"]')
.click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.removeFile('5_Renamed_Contract.sol', 'default_workspace')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItem5_Renamed_Contract.sol"')
},
@ -75,7 +71,7 @@ module.exports = {
.click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
},
@ -88,11 +84,11 @@ module.exports = {
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.pause(2000)
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.pause(2000)
.perform((done) => {
if (runtimeBrowser === 'chrome') {

@ -38,7 +38,7 @@ module.exports = {
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.pause(2000)
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.click('.modal-ok')
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.pause(10000)
.getText('[data-id="default_workspaceModalDialogModalBody-react"]', (result) => {
console.log(result)
@ -99,7 +99,7 @@ module.exports = {
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.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.')

@ -125,7 +125,7 @@ function runTests (browser: NightwatchBrowser) {
.pause(1000)
.renamePath('folder1/contract_' + browserName + '.sol', 'renamed_contract_' + browserName + '.sol', 'folder1/renamed_contract_' + browserName + '.sol')
.pause(1000)
.removeFile('folder1/contract_' + browserName + '_toremove.sol')
.removeFile('folder1/contract_' + browserName + '_toremove.sol', 'localhost')
.perform(function (done) {
testImportFromRemixd(browser, () => { done() })
})

@ -37,7 +37,7 @@ module.exports = {
.clickLaunchIcon('fileExplorers')
.pause(10000)
.openFile('tests/simple_storage_test.sol')
.removeFile('tests/simple_storage_test.sol')
.removeFile('tests/simple_storage_test.sol', 'default_workspace')
},
'Should run simple unit test `simple_storage_test.sol` ': function (browser: NightwatchBrowser) {

@ -18,6 +18,8 @@ module.exports = {
'Editor should be focused on the 3_Ballot.sol': function (browser: NightwatchBrowser) {
browser
.pause(5000)
.refresh()
.pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('contract Ballot {') !== -1, 'content doesn\'t include Ballot contract')
})
@ -32,18 +34,18 @@ module.exports = {
browser
.clickLaunchIcon('fileExplorers')
.click('*[data-id="workspaceCreate"]') // create workspace_name
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.clearValue('*[data-id="modalDialogCustomPromptText"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', 'workspace_name')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name' })
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.addFile('test.sol', { content: 'test' })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
.click('*[data-id="workspaceCreate"]') // create workspace_name_1
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.clearValue('*[data-id="modalDialogCustomPromptText"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', 'workspace_name_1')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name_1' })
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_name"]')

@ -41,7 +41,7 @@ declare module "nightwatch" {
getInstalledPlugins(cb: (plugins: string[]) => void): NightwatchBrowser,
verifyCallReturnValue(address: string, checks: string[]): NightwatchBrowser,
testEditorValue(testvalue: string): NightwatchBrowser,
removeFile(path: string): NightwatchBrowser,
removeFile(path: string, workspace: string): NightwatchBrowser,
switchBrowserWindow(url: string, windowName: string, cb: (browser: NightwatchBrowser, window?: NightwatchCallbackResult<Window>) => void): NightwatchBrowser,
setupMetamask(passphrase: string, password: string): NightwatchBrowser,
signMessage(msg: string, callback: (hash: { value: string }, signature: { value: string }) => void): NightwatchBrowser,

@ -444,7 +444,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
await appManager.activatePlugin(['contentImport', 'theme', 'editor', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await appManager.activatePlugin(['mainPanel', 'menuicons'])
await appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await appManager.activatePlugin(['home', 'hiddenPanel', 'pluginManager', 'fileExplorers', 'settings', 'contextualListener', 'terminal', 'fetchAndCompile'])
await appManager.activatePlugin(['home'])
await appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'fileExplorers', 'settings', 'contextualListener', 'terminal', 'fetchAndCompile'])
const queryParams = new QueryParams()
const params = queryParams.get()
@ -486,6 +487,5 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
migrateToWorkspace(fileManager)
filePanel.initWorkspace()
if (params.embed) framingService.embed()
}

@ -49,6 +49,10 @@ class FileManager extends Plugin {
this.init()
}
getOpenedFiles () {
return this.openedFiles
}
setMode (mode) {
this.mode = mode
}
@ -589,6 +593,12 @@ class FileManager extends Plugin {
if (!this.exists(workspaceRootPath)) await this.mkdir(workspaceRootPath)
if (!this.exists(workspacePath)) await this.mkdir(workspacePath)
}
async workspaceExists (name) {
const workspaceProvider = this._deps.filesProviders.workspace
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
return this.exists(workspacePath)
}
}
module.exports = FileManager

@ -54,6 +54,11 @@ module.exports = class RemixDProvider {
close (cb) {
this._isReady = false
cb()
this.event.trigger('disconnected')
}
preInit () {
this._registerEvent()
}
init (cb) {
@ -63,6 +68,7 @@ module.exports = class RemixDProvider {
this._isReady = true
this._readOnlyMode = result
this._registerEvent()
this.event.trigger('connected')
cb && cb()
}).catch((error) => {
cb && cb(error)

@ -30,15 +30,13 @@ const profile = {
}
export class RemixdHandle extends WebsocketPlugin {
constructor (fileSystemExplorer, locahostProvider, appManager) {
constructor (locahostProvider, appManager) {
super(profile)
this.fileSystemExplorer = fileSystemExplorer
this.locahostProvider = locahostProvider
this.appManager = appManager
}
deactivate () {
this.fileSystemExplorer.hide()
if (super.socket) super.deactivate()
this.call('manager', 'deactivatePlugin', 'git')
this.locahostProvider.close((error) => {
@ -82,9 +80,7 @@ export class RemixdHandle extends WebsocketPlugin {
this.canceled()
}
}, 3000)
this.locahostProvider.init(() => {
this.fileSystemExplorer.show()
})
this.locahostProvider.init(() => {})
this.call('manager', 'activatePlugin', 'git')
}
}

@ -3,10 +3,8 @@ 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 { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line
import './styles/file-panel-styles.css'
var ethutil = require('ethereumjs-util')
var yo = require('yo-yo')
import { Workspace } from '@remix-ui/workspace' // eslint-disable-line
import * as ethutil from 'ethereumjs-util'
var EventManager = require('../../lib/events')
var { RemixdHandle } = require('../files/remixd-handle.js')
var { GitHandle } = require('../files/git-handle.js')
@ -14,9 +12,6 @@ var globalRegistry = require('../../global/registry')
var examples = require('../editor/examples')
var GistHandler = require('../../lib/gist-handler')
var QueryParams = require('../../lib/query-params')
const modalDialog = require('../ui/modal-dialog-custom')
var canUpload = window.File || window.FileReader || window.FileList || window.Blob
/*
Overview of APIs:
@ -51,84 +46,107 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) {
super(profile)
this.event = new EventManager()
this._components = {}
this._components.registry = globalRegistry
this._deps = {
fileProviders: this._components.registry.get('fileproviders').api,
fileManager: this._components.registry.get('filemanager').api,
config: this._components.registry.get('config').api
}
this.LOCALHOST = ' - connect to localhost - '
this.NO_WORKSPACE = ' - none - '
this.hideRemixdExplorer = true
this.remixdExplorer = {
hide: () => {
if (this.currentWorkspace === this.LOCALHOST) this.setWorkspace(this.NO_WORKSPACE)
this._deps.fileManager.setMode('browser')
this.hideRemixdExplorer = true
this.renderComponent()
},
show: () => {
this._deps.fileManager.setMode('localhost')
this.hideRemixdExplorer = false
this.renderComponent()
}
fileManager: this._components.registry.get('filemanager').api
}
this.reset = false
this.registeredMenuItems = []
this.displayNewFile = false
this.uploadFileEvent = null
this.el = yo`
<div id="fileExplorerView">
</div>
`
this.remixdHandle = new RemixdHandle(this.remixdExplorer, this._deps.fileProviders.localhost, appManager)
this.el = document.createElement('div')
this.el.setAttribute('id', 'fileExplorerView')
this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle()
this.registeredMenuItems = []
this.request = {}
this.workspaces = []
this.initialWorkspace = null
}
this.event = new EventManager()
this._deps.fileProviders.localhost.event.register('connecting', (event) => {
})
render () {
this.initWorkspace().then(() => this.getWorkspaces()).catch(console.error)
return this.el
}
this._deps.fileProviders.localhost.event.register('connected', (event) => {
this.remixdExplorer.show()
})
renderComponent () {
ReactDOM.render(
<Workspace
createWorkspace={this.createWorkspace.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}
initialWorkspace={this.initialWorkspace}
/>
, this.el)
}
this._deps.fileProviders.localhost.event.register('errored', (event) => {
this.remixdExplorer.hide()
})
/**
* @param item { id: string, name: string, type?: string[], path?: string[], extension?: string[], pattern?: string[] }
* @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')
this._deps.fileProviders.localhost.event.register('closed', (event) => {
this.remixdExplorer.hide()
})
this.registeredMenuItems = [...this.registeredMenuItems, item]
this.renderComponent()
}
this.currentWorkspace = null
async getCurrentWorkspace () {
return await this.request.getCurrentWorkspace()
}
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
this._deps.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, fileTree) => {
if (error) return console.error(error)
this.setWorkspace(Object.keys(fileTree)[0].replace(workspacesPath + '/', ''))
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 + '/', '')))
})
})
this.workspaces = await result
this.renderComponent()
return this.workspaces
}
async initWorkspace () {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
const queryParams = new QueryParams()
const gistHandler = new GistHandler()
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
const params = queryParams.get()
// get the file from gist
const gistHandler = new GistHandler()
const loadedFromGist = gistHandler.loadFromGist(params, this._deps.fileManager)
if (loadedFromGist) return
if (params.code) {
try {
await this._deps.fileManager.createWorkspace('code-sample')
this._deps.fileProviders.workspace.setWorkspace('code-sample')
var hash = ethutil.bufferToHex(ethutil.keccak(params.code))
const fileName = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol'
const path = 'browser/' + workspacesPath + '/code-sample/' + fileName
await this._deps.fileManager.writeFile(path, atob(params.code))
this.setWorkspace('code-sample')
await this._deps.fileManager.openFile(path)
this.initialWorkspace = 'code-sample'
await this._deps.fileManager.openFile(fileName)
} catch (e) {
console.error(e)
}
@ -138,241 +156,54 @@ module.exports = class Filepanel extends ViewPlugin {
this._deps.fileProviders.browser.resolveDirectory('/', async (error, filesList) => {
if (error) console.error(error)
if (Object.keys(filesList).length === 0) {
for (const file in examples) {
await this._deps.fileManager.writeFile('browser/' + workspacesPath + '/default_workspace/' + examples[file].name, examples[file].content)
}
this.setWorkspace('default_workspace')
await this.createWorkspace('default_workspace')
}
this.getWorkspaces()
})
}
async refreshWorkspacesList () {
if (!document.getElementById('workspacesSelect')) return
const workspaces = await this.getWorkspaces()
workspaces.push(this.LOCALHOST)
workspaces.push(this.NO_WORKSPACE)
ReactDOM.render(
(
workspaces
.map((folder) => {
return <option selected={this.currentWorkspace === folder} value={folder}>{folder}</option>
})), document.getElementById('workspacesSelect')
)
}
resetFocus (value) {
this.reset = value
this.renderComponent()
async createNewFile () {
return await this.request.createNewFile()
}
createNewFile () {
this.displayNewFile = true
this.renderComponent()
async uploadFile () {
return await this.request.uploadFile()
}
resetNewFile () {
this.displayNewFile = false
this.renderComponent()
}
uploadFile (target) {
this.uploadFileEvent = target
this.renderComponent()
}
resetUploadFile () {
this.uploadFileEvent = null
this.renderComponent()
}
render () {
return this.el
}
getWorkspaces () {
return new Promise((resolve, reject) => {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
this._deps.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 + '/', '')))
})
})
}
getCurrentWorkspace () {
return this.currentWorkspace
async createWorkspace (workspaceName) {
if (await this._deps.fileManager.workspaceExists(workspaceName)) throw new Error('workspace already exists')
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
await this._deps.fileManager.createWorkspace(workspaceName)
for (const file in examples) {
try {
await this._deps.fileManager.writeFile('browser/' + workspacesPath + '/' + workspaceName + '/' + examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}
}
async setWorkspace (name) {
/** these are called by the react component, action is already finished whent it's called */
async setWorkspace (workspace) {
this._deps.fileManager.removeTabsOf(this._deps.fileProviders.workspace)
this.currentWorkspace = name
if (name === this.LOCALHOST) {
this._deps.fileProviders.workspace.clearWorkspace()
if (workspace.isLocalhost) {
this.call('manager', 'activatePlugin', 'remixd')
} else if (name === this.NO_WORKSPACE) {
this._deps.fileProviders.workspace.clearWorkspace()
} else {
this._deps.fileProviders.workspace.setWorkspace(name)
}
if (name !== this.LOCALHOST && await this.call('manager', 'isActive', 'remixd')) {
} else if (await this.call('manager', 'isActive', 'remixd')) {
this.call('manager', 'deactivatePlugin', 'remixd')
}
this.renderComponent()
this.emit('setWorkspace', { name })
this.emit('setWorkspace', workspace)
}
/**
*
* @param item { id: string, name: string, type?: string[], path?: string[], extension?: string[], pattern?: string[] }
* @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')
this.registeredMenuItems = [...this.registeredMenuItems, item]
this.renderComponent()
workspaceRenamed (workspace) {
this.emit('renameWorkspace', workspace)
}
renameWorkspace () {
modalDialog.prompt('Rename Workspace', 'Please choose a name for the workspace', this.currentWorkspace, async (value) => {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
await this._deps.fileManager.rename('browser/' + workspacesPath + '/' + this.currentWorkspace, 'browser/' + workspacesPath + '/' + value)
this.setWorkspace(value)
this.emit('renameWorkspace', { name: value })
})
}
createWorkspace () {
return new Promise((resolve, reject) => {
const workspace = `workspace_${Date.now()}`
modalDialog.prompt('New Workspace', 'Please choose a name for the workspace', workspace, (value) => {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
this._deps.fileProviders.browser.createDir(workspacesPath + '/' + value, async () => {
this.setWorkspace(value)
for (const file in examples) {
await this._deps.fileManager.writeFile(`${examples[file].name}`, examples[file].content)
}
resolve(value)
})
}, () => reject(new Error('workspace creation rejected by user')))
})
}
deleteCurrentWorkspace () {
if (!this.currentWorkspace) return
modalDialog.confirm('Delete Workspace', 'Please confirm workspace deletion', () => {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
this._deps.fileProviders.browser.remove(workspacesPath + '/' + this.currentWorkspace)
const name = this.currentWorkspace
this.currentWorkspace = null
this.setWorkspace(this.NO_WORKSPACE)
this.renderComponent()
this.emit('deleteWorkspace', { name })
})
workspaceDeleted (workspace) {
this.emit('deleteWorkspace', workspace)
}
renderComponent () {
ReactDOM.render(
<div className='remixui_container'>
<div className='remixui_fileexplorer' onClick={() => this.resetFocus(true)}>
<div>
<header>
<div className="mb-2">
<label className="form-check-label" htmlFor="workspacesSelect">
Workspaces
</label>
<span className="remixui_menu">
<span
id='workspaceCreate'
data-id='workspaceCreate'
onClick={(e) => {
e.stopPropagation()
this.createWorkspace()
}}
className='far fa-plus-square remixui_menuicon'
title='Create a new Workspace'>
</span>
<span
hidden={this.currentWorkspace === this.LOCALHOST || this.currentWorkspace === this.NO_WORKSPACE}
id='workspaceRename'
data-id='workspaceRename'
onClick={(e) => {
e.stopPropagation()
this.renameWorkspace()
}}
className='far fa-edit remixui_menuicon'
title='Rename current Workspace'>
</span>
<span
hidden={this.currentWorkspace === this.LOCALHOST || this.currentWorkspace === this.NO_WORKSPACE}
id='workspaceDelete'
data-id='workspaceDelete'
onClick={(e) => {
e.stopPropagation()
this.deleteCurrentWorkspace()
}}
className='fas fa-trash'
title='Delete current Workspace'>
</span>
</span>
<select id="workspacesSelect" data-id="workspacesSelect" onChange={(e) => this.setWorkspace(e.target.value)} className="form-control custom-select">
</select>
</div>
</header>
</div>
<div className='remixui_fileExplorerTree'>
<div>
<div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'>
{ this.hideRemixdExplorer && this.currentWorkspace && this.currentWorkspace !== this.NO_WORKSPACE &&
<FileExplorer
name={this.currentWorkspace}
registry={this._components.registry}
filesProvider={this._deps.fileProviders.workspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={this}
focusRoot={this.reset}
contextMenuItems={this.registeredMenuItems}
/>
}
</div>
<div className='pl-2 filesystemexplorer remixui_treeview'>
{ !this.hideRemixdExplorer &&
<FileExplorer
name='localhost'
registry={this._components.registry}
filesProvider={this._deps.fileProviders.localhost}
menuItems={['createNewFile', 'createNewFolder']}
plugin={this}
focusRoot={this.reset}
contextMenuItems={this.registeredMenuItems}
/>
}
</div>
<div className='pl-2 remixui_treeview'>
{ false && <FileExplorer
name='browser'
registry={this._components.registry}
filesProvider={this._deps.fileProviders.browser}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={this}
focusRoot={this.reset}
contextMenuItems={this.registeredMenuItems}
displayInput={this.displayNewFile}
externalUploads={this.uploadFileEvent}
/>
}
</div>
</div>
</div>
</div>
</div>
, this.el)
setTimeout(() => {
this.refreshWorkspacesList()
}, 500)
workspaceCreated (workspace) {
this.emit('createWorkspace', workspace)
}
/** end section */
}

@ -1,59 +0,0 @@
.remixui_container {
display : flex;
flex-direction : row;
width : 100%;
height : 100%;
box-sizing : border-box;
}
.remixui_fileexplorer {
display : flex;
flex-direction : column;
position : relative;
width : 100%;
padding-left : 6px;
padding-top : 6px;
}
.remixui_fileExplorerTree {
cursor : default;
}
.remixui_gist {
padding : 10px;
}
.remixui_gist i {
cursor : pointer;
}
.remixui_gist i:hover {
color : orange;
}
.remixui_connectToLocalhost {
padding : 10px;
}
.remixui_connectToLocalhost i {
cursor : pointer;
}
.remixui_connectToLocalhost i:hover {
color : var(--secondary)
}
.remixui_uploadFile {
padding : 10px;
}
.remixui_uploadFile label:hover {
color : var(--secondary)
}
.remixui_uploadFile label {
cursor : pointer;
}
.remixui_treeview {
overflow-y : auto;
}
.remixui_dialog {
display: flex;
flex-direction: column;
}
.remixui_dialogParagraph {
margin-bottom: 2em;
word-break: break-word;
}
.remixui_menuicon {
padding-right : 10px;
}

@ -40,6 +40,13 @@ export const ModalDialog = (props: ModalDialogProps) => {
handleHide()
}
const handleBlur = (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
e.stopPropagation()
handleHide()
}
}
return (
<div
data-id={`${props.id}ModalDialogContainer-react`}
@ -51,10 +58,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
>
<div className="modal-dialog" role="document">
<div
onBlur={(e) => {
e.stopPropagation()
handleHide()
}}
onBlur={handleBlur}
ref={modal}
tabIndex={-1}
className={'modal-content remixModalContent ' + (props.modalClass ? props.modalClass : '')}

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

@ -0,0 +1,19 @@
{
"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"
}
}

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

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

@ -1,14 +1,11 @@
var csjs = require('csjs-inject')
var css = csjs`
.container {
.remixui_container {
display : flex;
flex-direction : row;
width : 100%;
height : 100%;
box-sizing : border-box;
}
.fileexplorer {
.remixui_fileexplorer {
display : flex;
flex-direction : column;
position : relative;
@ -16,47 +13,48 @@ var css = csjs`
padding-left : 6px;
padding-top : 6px;
}
.fileExplorerTree {
.remixui_fileExplorerTree {
cursor : default;
}
.gist {
.remixui_gist {
padding : 10px;
}
.gist i {
.remixui_gist i {
cursor : pointer;
}
.gist i:hover {
.remixui_gist i:hover {
color : orange;
}
.connectToLocalhost {
.remixui_connectToLocalhost {
padding : 10px;
}
.connectToLocalhost i {
.remixui_connectToLocalhost i {
cursor : pointer;
}
.connectToLocalhost i:hover {
.remixui_connectToLocalhost i:hover {
color : var(--secondary)
}
.uploadFile {
.remixui_uploadFile {
padding : 10px;
}
.uploadFile label:hover {
.remixui_uploadFile label:hover {
color : var(--secondary)
}
.uploadFile label {
.remixui_uploadFile label {
cursor : pointer;
}
.treeview {
.remixui_treeview {
overflow-y : auto;
}
.dialog {
.remixui_dialog {
display: flex;
flex-direction: column;
}
.dialogParagraph {
.remixui_dialogParagraph {
margin-bottom: 2em;
word-break: break-word;
}
`
module.exports = css
.remixui_menuicon {
padding-right : 10px;
}

@ -0,0 +1,399 @@
import React, { useState, useEffect, useRef } from 'react';
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
/* eslint-disable-next-line */
export interface WorkspaceProps {
setWorkspace: ({ name: string, isLocalhost: boolean }) => void,
createWorkspace: (name: 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: [] // menu items
initialWorkspace: string
}
var canUpload = window.File || window.FileReader || window.FileList || window.Blob
export const Workspace = (props: WorkspaceProps) => {
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
/* extends the parent 'plugin' with some function needed by the file explorer */
props.plugin.resetFocus = () => {
setState(prevState => {
return { ...prevState, reset: true }
})
}
props.plugin.resetNewFile = () => {
setState(prevState => {
return { ...prevState, displayNewFile: !state.displayNewFile }
})
}
/* implement an external API, consumed by the parent */
props.request.createWorkspace = () => {
return createWorkspace()
}
props.request.createNewFile = () => {
props.plugin.resetNewFile()
}
props.request.uploadFile = (target) => {
setState(prevState => {
return { ...prevState, uploadFileEvent: target }
})
}
props.request.getCurrentWorkspace = () => {
return state.currentWorkspace
}
useEffect(() => {
const getWorkspaces = async () => {
if (props.workspaces && Array.isArray(props.workspaces)) {
if (props.initialWorkspace) {
props.workspace.setWorkspace(props.initialWorkspace)
setState(prevState => {
return { ...prevState, workspaces: props.workspaces, currentWorkspace: props.initialWorkspace }
})
} else if (props.workspaces.length > 0 && state.currentWorkspace === NO_WORKSPACE) {
props.workspace.setWorkspace(props.workspaces[0])
setState(prevState => {
return { ...prevState, workspaces: props.workspaces, currentWorkspace: props.workspaces[0] }
})
} else {
setState(prevState => {
return { ...prevState, workspaces: props.workspaces }
})
}
}
}
getWorkspaces()
}, [props.workspaces])
useEffect(() => {
props.localhost.event.register('connected', (event) => {
remixdExplorer.show()
})
props.localhost.event.register('disconnected', (event) => {
remixdExplorer.hide()
})
}, [])
const [state, setState] = useState({
workspaces: [],
reset: false,
currentWorkspace: NO_WORKSPACE,
hideRemixdExplorer: true,
displayNewFile: false,
externalUploads: null,
uploadFileEvent: null,
modal: {
hide: true,
title: '',
message: null,
ok: {
label: '',
fn: () => {}
},
cancel: {
label: '',
fn: () => {}
},
handleHide: null
}
})
/* workspace creation, renaming and deletion */
const renameCurrentWorkspace = () => {
modal('Rename Workspace', renameModalMessage(), {
label: 'OK',
fn: onFinishRenameWorkspace
}, {
label: '',
fn: () => {}
})
}
const createWorkspace = () => {
modal('Create Workspace', createModalMessage(), {
label: 'OK',
fn: onFinishCreateWorkspace
}, {
label: '',
fn: () => {}
})
}
const deleteCurrentWorkspace = () => {
modal('Remove Workspace', 'Please choose a name for the workspace', {
label: 'OK',
fn: onFinishDeleteWorkspace
}, {
label: '',
fn: () => {}
})
}
const modalMessage = (title: string, body: string) => {
modal(title, body, {
label: 'OK',
fn: () => {}
}, {
label: null,
fn: null
})
}
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
const workspacesPath = props.workspace.workspacesPath
await props.fileManager.rename('browser/' + workspacesPath + '/' + state.currentWorkspace, 'browser/' + workspacesPath + '/' + workspaceName)
setWorkspace(workspaceName)
props.workspaceRenamed({ name: state.currentWorkspace })
}
const onFinishCreateWorkspace = async () => {
if (workspaceCreateInput.current === undefined) return
// @ts-ignore: Object is possibly 'null'.
const workspaceName = workspaceCreateInput.current.value
try {
await props.createWorkspace(workspaceName)
} catch (e) {
modalMessage('Workspace Creation', e.message)
console.error(e)
}
await setWorkspace(workspaceName)
}
const onFinishDeleteWorkspace = async () => {
const workspacesPath = props.workspace.workspacesPath
props.browser.remove(workspacesPath + '/' + state.currentWorkspace)
const name = state.currentWorkspace
setWorkspace(NO_WORKSPACE)
props.workspaceDeleted({ name })
}
/**** ****/
const resetFocus = (reset) => {
setState(prevState => {
return { ...prevState, reset }
})
}
const setWorkspace = async (name) => {
if (name === LOCALHOST) {
props.workspace.clearWorkspace()
} else if (name === NO_WORKSPACE) {
props.workspace.clearWorkspace()
} else {
props.workspace.setWorkspace(name)
}
props.plugin.getWorkspaces()
setState(prevState => {
return { ...prevState, currentWorkspace: name }
})
props.setWorkspace({ name, isLocalhost: name === LOCALHOST })
}
const remixdExplorer = {
hide: () => {
if (state.currentWorkspace === LOCALHOST) setWorkspace(NO_WORKSPACE)
props.fileManager.setMode('browser')
setState(prevState => {
return { ...prevState, hideRemixdExplorer: true }
})
},
show: () => {
props.fileManager.setMode('localhost')
setState(prevState => {
return { ...prevState, hideRemixdExplorer: false }
})
}
}
const handleHideModal = () => {
setState(prevState => {
return { ...prevState, modal: { ...state.modal, hide: true, message: null } }
})
}
const modal = async (title: string, message: string | JSX.Element, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }) => {
await setState(prevState => {
return {
...prevState,
modal: {
...prevState.modal,
hide: false,
message,
title,
ok,
cancel,
handleHide: handleHideModal
}
}
})
}
const createModalMessage = () => {
return (
<>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextCreate" placeholder={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" />
</>
)
}
const renameModalMessage = () => {
return (
<>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextRename" placeholder={ state.currentWorkspace } ref={workspaceRenameInput} className="form-control" />
</>
)
}
// const handleWorkspaceSelect = (e) => {
// const value = e.target.value
// setWorkspace(value)
// }
return (
<div className='remixui_container'>
<ModalDialog
id='workspacesModalDialog'
title={ state.modal.title }
message={ state.modal.message }
hide={ state.modal.hide }
ok={ state.modal.ok }
cancel={ state.modal.cancel }
handleHide={ handleHideModal }>
{ (typeof state.modal.message !== 'string') && state.modal.message }
</ModalDialog>
<div className='remixui_fileexplorer' onClick={() => resetFocus(true)}>
<div>
<header>
<div className="mb-2">
<label className="form-check-label" htmlFor="workspacesSelect">
Workspaces
</label>
<span className="remixui_menu">
<span
id='workspaceCreate'
data-id='workspaceCreate'
onClick={(e) => {
e.stopPropagation()
createWorkspace()
}}
className='far fa-plus-square remixui_menuicon'
title='Create a new Workspace'>
</span>
<span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE}
id='workspaceRename'
data-id='workspaceRename'
onClick={(e) => {
e.stopPropagation()
renameCurrentWorkspace()
}}
className='far fa-edit remixui_menuicon'
title='Rename current Workspace'>
</span>
<span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE}
id='workspaceDelete'
data-id='workspaceDelete'
onClick={(e) => {
e.stopPropagation()
deleteCurrentWorkspace()
}}
className='fas fa-trash'
title='Delete current Workspace'>
</span>
</span>
<select id="workspacesSelect" data-id="workspacesSelect" onChange={(e) => setWorkspace(e.target.value)} className="form-control custom-select">
{
state.workspaces
.map((folder) => {
return <option selected={state.currentWorkspace === folder} value={folder}>{folder}</option>
})
}
<option selected={state.currentWorkspace === LOCALHOST} value={LOCALHOST}>{LOCALHOST}</option>
{ state.workspaces.length <= 0 && <option selected={state.currentWorkspace === NO_WORKSPACE} value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
</select>
</div>
</header>
</div>
<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 &&
<FileExplorer
name={state.currentWorkspace}
registry={props.registry}
filesProvider={props.workspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
displayInput={state.displayNewFile}
externalUploads={state.uploadFileEvent}
/>
}
</div>
<div className='pl-2 filesystemexplorer remixui_treeview'>
{ !state.hideRemixdExplorer &&
<FileExplorer
name='localhost'
registry={props.registry}
filesProvider={props.localhost}
menuItems={['createNewFile', 'createNewFolder']}
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
/>
}
</div>
<div className='pl-2 remixui_treeview'>
{ false && <FileExplorer
name='browser'
registry={props.registry}
filesProvider={props.browser}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
displayInput={state.displayNewFile}
externalUploads={state.uploadFileEvent}
/>
}
</div>
</div>
</div>
</div>
</div>
);
};
export default Workspace;

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"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"]
}

@ -92,6 +92,9 @@
},
"debugger": {
"tags": []
},
"remix-ui-workspace": {
"tags": []
}
}
}

@ -20,7 +20,9 @@
"@remix-project/remix-astwalker": ["dist/libs/remix-astwalker/index.js"],
"@remix-project/remix-debug": ["dist/libs/remix-debug/src/index.js"],
"@remix-project/remix-lib": ["dist/libs/remix-lib/src/index.js"],
"@remix-project/remix-simulator": ["dist/libs/remix-simulator/src/index.js"],
"@remix-project/remix-simulator": [
"dist/libs/remix-simulator/src/index.js"
],
"@remix-project/remix-solidity": ["dist/libs/remix-solidity/index.js"],
"@remix-project/remix-tests": ["dist/libs/remix-tests/src/index.js"],
"@remix-project/remix-url-resolver": [
@ -35,7 +37,8 @@
"@remix-project/remix-solidity-ts": ["libs/remix-solidity/src/index.ts"],
"@remix-ui/modal-dialog": ["libs/remix-ui/modal-dialog/src/index.ts"],
"@remix-ui/toaster": ["libs/remix-ui/toaster/src/index.ts"],
"@remix-ui/file-explorer": ["libs/remix-ui/file-explorer/src/index.ts"]
"@remix-ui/file-explorer": ["libs/remix-ui/file-explorer/src/index.ts"],
"@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]

@ -347,7 +347,11 @@
"linter": "eslint",
"config": "libs/remix-tests/.eslintrc",
"tsConfig": ["libs/remix-tests/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "libs/remix-tests/tests/**/*", "**/dist/**"]
"exclude": [
"**/node_modules/**",
"libs/remix-tests/tests/**/*",
"**/dist/**"
]
}
},
"test": {
@ -705,6 +709,22 @@
}
}
}
},
"remix-ui-workspace": {
"root": "libs/remix-ui/workspace",
"sourceRoot": "libs/remix-ui/workspace/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/workspace/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/workspace/**/*"]
}
}
}
}
},
"cli": {

Loading…
Cancel
Save