diff --git a/apps/remix-ide-e2e/src/commands/removeFile.ts b/apps/remix-ide-e2e/src/commands/removeFile.ts index 91ba3d2216..8f0e2c9ee2 100644 --- a/apps/remix-ide-e2e/src/commands/removeFile.ts +++ b/apps/remix-ide-e2e/src/commands/removeFile.ts @@ -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() }) diff --git a/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts b/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts index 93b7d8d055..aa54c740d9 100644 --- a/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts +++ b/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts @@ -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') { diff --git a/apps/remix-ide-e2e/src/tests/gist.test.ts b/apps/remix-ide-e2e/src/tests/gist.test.ts index 4b6e191ada..4ebcb17e22 100644 --- a/apps/remix-ide-e2e/src/tests/gist.test.ts +++ b/apps/remix-ide-e2e/src/tests/gist.test.ts @@ -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.') diff --git a/apps/remix-ide-e2e/src/tests/remixd.test.ts b/apps/remix-ide-e2e/src/tests/remixd.test.ts index bf72bcb4af..818834b837 100644 --- a/apps/remix-ide-e2e/src/tests/remixd.test.ts +++ b/apps/remix-ide-e2e/src/tests/remixd.test.ts @@ -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() }) }) diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 9ec8c7585d..8ed2635e1d 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -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) { diff --git a/apps/remix-ide-e2e/src/tests/workspace.test.ts b/apps/remix-ide-e2e/src/tests/workspace.test.ts index cc6eb3d99b..6ce4c81c60 100644 --- a/apps/remix-ide-e2e/src/tests/workspace.test.ts +++ b/apps/remix-ide-e2e/src/tests/workspace.test.ts @@ -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"]') diff --git a/apps/remix-ide-e2e/src/types/index.d.ts b/apps/remix-ide-e2e/src/types/index.d.ts index aac4cccdfb..a2423ab159 100644 --- a/apps/remix-ide-e2e/src/types/index.d.ts +++ b/apps/remix-ide-e2e/src/types/index.d.ts @@ -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) => void): NightwatchBrowser, setupMetamask(passphrase: string, password: string): NightwatchBrowser, signMessage(msg: string, callback: (hash: { value: string }, signature: { value: string }) => void): NightwatchBrowser, diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index f4f0f02eb0..7502e3541e 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -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() } diff --git a/apps/remix-ide/src/app/files/fileManager.js b/apps/remix-ide/src/app/files/fileManager.js index a9bbc771da..6f971f3331 100644 --- a/apps/remix-ide/src/app/files/fileManager.js +++ b/apps/remix-ide/src/app/files/fileManager.js @@ -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 diff --git a/apps/remix-ide/src/app/files/remixDProvider.js b/apps/remix-ide/src/app/files/remixDProvider.js index 5f01a4673f..5c8963baac 100644 --- a/apps/remix-ide/src/app/files/remixDProvider.js +++ b/apps/remix-ide/src/app/files/remixDProvider.js @@ -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) diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/files/remixd-handle.js index e65c9c404d..b068c0475d 100644 --- a/apps/remix-ide/src/app/files/remixd-handle.js +++ b/apps/remix-ide/src/app/files/remixd-handle.js @@ -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') } } diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 7f9596b066..8bf74a6756 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -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` -
-
- ` - 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( + + , 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 - })), 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( -
-
this.resetFocus(true)}> -
-
-
- - - { - e.stopPropagation() - this.createWorkspace() - }} - className='far fa-plus-square remixui_menuicon' - title='Create a new Workspace'> - - - - - -
-
-
-
-
-
- { this.hideRemixdExplorer && this.currentWorkspace && this.currentWorkspace !== this.NO_WORKSPACE && - - } -
-
- { !this.hideRemixdExplorer && - - } -
-
- { false && - } -
-
-
-
-
- , this.el) - setTimeout(() => { - this.refreshWorkspacesList() - }, 500) + workspaceCreated (workspace) { + this.emit('createWorkspace', workspace) } + /** end section */ } diff --git a/apps/remix-ide/src/app/panels/styles/file-panel-styles.css b/apps/remix-ide/src/app/panels/styles/file-panel-styles.css deleted file mode 100644 index 8744e6a18d..0000000000 --- a/apps/remix-ide/src/app/panels/styles/file-panel-styles.css +++ /dev/null @@ -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; -} diff --git a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx index 410cb768e3..2caa41219d 100644 --- a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx +++ b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx @@ -40,6 +40,13 @@ export const ModalDialog = (props: ModalDialogProps) => { handleHide() } + const handleBlur = (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + e.stopPropagation() + handleHide() + } + } + return (
{ >
{ - e.stopPropagation() - handleHide() - }} + onBlur={handleBlur} ref={modal} tabIndex={-1} className={'modal-content remixModalContent ' + (props.modalClass ? props.modalClass : '')} diff --git a/libs/remix-ui/workspace/.babelrc b/libs/remix-ui/workspace/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/workspace/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/workspace/.eslintrc b/libs/remix-ui/workspace/.eslintrc new file mode 100644 index 0000000000..dae5c6feeb --- /dev/null +++ b/libs/remix-ui/workspace/.eslintrc @@ -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" + } +} diff --git a/libs/remix-ui/workspace/README.md b/libs/remix-ui/workspace/README.md new file mode 100644 index 0000000000..61389a2bf8 --- /dev/null +++ b/libs/remix-ui/workspace/README.md @@ -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). diff --git a/libs/remix-ui/workspace/src/index.ts b/libs/remix-ui/workspace/src/index.ts new file mode 100644 index 0000000000..b6c5ded2a1 --- /dev/null +++ b/libs/remix-ui/workspace/src/index.ts @@ -0,0 +1 @@ +export * from './lib/remix-ui-workspace'; diff --git a/apps/remix-ide/src/app/panels/styles/file-panel-styles.js b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.css similarity index 62% rename from apps/remix-ide/src/app/panels/styles/file-panel-styles.js rename to libs/remix-ui/workspace/src/lib/remix-ui-workspace.css index 4db78296b9..f190b84fac 100644 --- a/apps/remix-ide/src/app/panels/styles/file-panel-styles.js +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.css @@ -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; + } + \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx new file mode 100644 index 0000000000..a23e143a0a --- /dev/null +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -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 ( + <> + { state.modal.message } + + + ) + } + + const renameModalMessage = () => { + return ( + <> + { state.modal.message } + + + ) + } + + // const handleWorkspaceSelect = (e) => { + // const value = e.target.value + + // setWorkspace(value) + // } + + return ( +
+ + { (typeof state.modal.message !== 'string') && state.modal.message } + +
resetFocus(true)}> +
+
+
+ + + { + e.stopPropagation() + createWorkspace() + }} + className='far fa-plus-square remixui_menuicon' + title='Create a new Workspace'> + + + + + +
+
+
+
+
+
+ { state.hideRemixdExplorer && state.currentWorkspace && state.currentWorkspace !== NO_WORKSPACE && state.currentWorkspace !== LOCALHOST && + + } +
+
+ { !state.hideRemixdExplorer && + + } +
+
+ { false && + } +
+
+
+
+
+ ); +}; + +export default Workspace; diff --git a/libs/remix-ui/workspace/tsconfig.json b/libs/remix-ui/workspace/tsconfig.json new file mode 100644 index 0000000000..6b65264565 --- /dev/null +++ b/libs/remix-ui/workspace/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/remix-ui/workspace/tsconfig.lib.json b/libs/remix-ui/workspace/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/workspace/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"] +} diff --git a/nx.json b/nx.json index 2d9caab10b..5cf047d9cc 100644 --- a/nx.json +++ b/nx.json @@ -92,6 +92,9 @@ }, "debugger": { "tags": [] + }, + "remix-ui-workspace": { + "tags": [] } } } diff --git a/tsconfig.json b/tsconfig.json index 5a873c9f97..c2a383e9de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] diff --git a/workspace.json b/workspace.json index 3b1e4d5880..10793e13b3 100644 --- a/workspace.json +++ b/workspace.json @@ -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": {