diff --git a/apps/remix-ide-e2e/src/tests/gist.test.ts b/apps/remix-ide-e2e/src/tests/gist.test.ts index 4ebcb17e22..f832379e1f 100644 --- a/apps/remix-ide-e2e/src/tests/gist.test.ts +++ b/apps/remix-ide-e2e/src/tests/gist.test.ts @@ -84,7 +84,7 @@ module.exports = { .setValue('*[data-id="modalDialogCustomPromptText"]', testData.invalidGistId) .modalFooterOKClick() .waitForElementVisible('*[data-id="modalDialogModalBody"]') - .assert.containsText('*[data-id="modalDialogModalBody"]', 'Gist load error: Not Found') + .assert.containsText('*[data-id="modalDialogModalBody"]', 'Not Found') .modalFooterOKClick() }, diff --git a/apps/remix-ide-e2e/src/tests/solidityImport.test.ts b/apps/remix-ide-e2e/src/tests/solidityImport.test.ts index adad6482ed..afbf1fe84f 100644 --- a/apps/remix-ide-e2e/src/tests/solidityImport.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityImport.test.ts @@ -21,6 +21,7 @@ module.exports = { .addFile('Untitled2.sol', sources[1]['Untitled2.sol']) .openFile('Untitled1.sol') .verifyContracts(['test6', 'test4', 'test5']) + .pause(1000) }, 'Test Failed Import': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 8ed2635e1d..284adb9efa 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -149,12 +149,14 @@ module.exports = { .addFile('myTests/simple_storage_test.sol', sources[0]['tests/simple_storage_test.sol']) .clickLaunchIcon('solidityUnitTesting') .setValue('*[data-id="uiPathInput"]', 'myTests') + .click('*[data-id="testTabGenerateTestFolder"]') .clickElementAtPosition('.singleTestLabel', 0) .scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]') .waitForElementPresent('*[data-id="testTabSolidityUnitTestsOutputheader"]', 40000) .waitForElementPresent('*[data-id="testTabSolidityUnitTestsOutput"]') .clearValue('*[data-id="uiPathInput"]') .setValue('*[data-id="uiPathInput"]', 'tests') + .click('*[data-id="testTabGenerateTestFolder"]') }, 'Solidity Unittests': 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 6ce4c81c60..15b435c890 100644 --- a/apps/remix-ide-e2e/src/tests/workspace.test.ts +++ b/apps/remix-ide-e2e/src/tests/workspace.test.ts @@ -50,6 +50,29 @@ module.exports = { .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') + }, + + 'Should rename a workspace': function (browser: NightwatchBrowser) { + browser + .click('*[data-id="workspaceRename"]') // rename workspace_name + .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') + .waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]') + // eslint-disable-next-line dot-notation + .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextRename"]')['value'] = 'workspace_name_renamed' }) + .click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') + .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') + .click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') + .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') + }, + + 'Should delete a workspace': function (browser: NightwatchBrowser) { + browser + .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .click('*[data-id="workspaceDelete"]') // delete workspace_name_1 + .waitForElementVisible('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') + .click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') + .waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .end() }, diff --git a/apps/remix-ide/src/app/files/fileManager.js b/apps/remix-ide/src/app/files/fileManager.js index d1838738e2..0ffe4f714b 100644 --- a/apps/remix-ide/src/app/files/fileManager.js +++ b/apps/remix-ide/src/app/files/fileManager.js @@ -514,6 +514,8 @@ class FileManager extends Plugin { if (file.startsWith('browser')) { return this._deps.filesProviders.browser } + const provider = this._deps.filesProviders.workspace + if (!provider.isReady()) throw createError({ code: 'ECONNRESET', message: 'No workspace has been opened.' }) return this._deps.filesProviders.workspace } @@ -579,7 +581,11 @@ class FileManager extends Plugin { async.each(Object.keys(filesSet), (file, callback) => { if (override) { - self._deps.filesProviders[fileProvider].set(file, filesSet[file].content) + try { + self._deps.filesProviders[fileProvider].set(file, filesSet[file].content) + } catch (e) { + return callback(e.message || e) + } self.syncEditor(fileProvider + file) return callback() } @@ -591,7 +597,11 @@ class FileManager extends Plugin { } else if (helper.checkSpecialChars(name)) { modalDialogCustom.alert('Special characters are not allowed') } else { - self._deps.filesProviders[fileProvider].set(name, filesSet[file].content) + try { + self._deps.filesProviders[fileProvider].set(name, filesSet[file].content) + } catch (e) { + return callback(e.message || e) + } self.syncEditor(fileProvider + name) } callback() diff --git a/apps/remix-ide/src/app/files/workspaceFileProvider.js b/apps/remix-ide/src/app/files/workspaceFileProvider.js index 9f8ae10eba..8522f5ff4c 100644 --- a/apps/remix-ide/src/app/files/workspaceFileProvider.js +++ b/apps/remix-ide/src/app/files/workspaceFileProvider.js @@ -6,6 +6,7 @@ class WorkspaceFileProvider extends FileProvider { constructor () { super('') this.workspacesPath = '.workspaces' + this.workspace = null } setWorkspace (workspace) { @@ -13,11 +14,20 @@ class WorkspaceFileProvider extends FileProvider { this.workspace = workspace } + getWorkspace () { + return this.workspace + } + + isReady () { + return this.workspace !== null + } + clearWorkspace () { this.workspace = null } removePrefix (path) { + if (!this.workspace) throw new Error('No workspace has been opened.') path = path.replace(/^\/|\/$/g, '') // remove first and last slash if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path if (path.startsWith(this.workspace)) return this.workspacesPath + '/' + this.workspace @@ -27,6 +37,7 @@ class WorkspaceFileProvider extends FileProvider { } resolveDirectory (path, callback) { + if (!this.workspace) throw new Error('No workspace has been opened.') super.resolveDirectory(path, (error, files) => { if (error) return callback(error) const unscoped = {} @@ -38,6 +49,7 @@ class WorkspaceFileProvider extends FileProvider { } _normalizePath (path) { + if (!this.workspace) throw new Error('No workspace has been opened.') return path.replace(this.workspacesPath + '/' + this.workspace + '/', '') } } diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index dad049d335..e247b66326 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -5,6 +5,7 @@ import React from 'react' // eslint-disable-line import ReactDOM from 'react-dom' import { Workspace } from '@remix-ui/workspace' // eslint-disable-line import * as ethutil from 'ethereumjs-util' +import { checkSpecialChars, checkSlash } from '../../lib/helper' var EventManager = require('../../lib/events') var { RemixdHandle } = require('../files/remixd-handle.js') var { GitHandle } = require('../files/git-handle.js') @@ -74,6 +75,7 @@ module.exports = class Filepanel extends ViewPlugin { ReactDOM.render( { if (e.keyCode === 191) this.updateDirList() }} onchange=${(e) => this.updateCurrentPath(e)}/>` + const createTestFolder = yo`` + const availablePaths = yo`
- ${this.inputPath} - ${this.uiPathList} +
+ ${this.inputPath} + ${createTestFolder} + ${this.uiPathList} +
` this.updateDirList() diff --git a/apps/remix-ide/src/app/tabs/testTab/testTab.js b/apps/remix-ide/src/app/tabs/testTab/testTab.js index 3132cba452..77c8fb4102 100644 --- a/apps/remix-ide/src/app/tabs/testTab/testTab.js +++ b/apps/remix-ide/src/app/tabs/testTab/testTab.js @@ -11,6 +11,9 @@ class TestTabLogic { setCurrentPath (path) { if (path.indexOf('/') === 0) return this.currentPath = path + } + + generateTestFolder (path) { const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) fileProvider.exists(path, (e, res) => { if (!res) fileProvider.createDir(path) }) } @@ -44,9 +47,9 @@ class TestTabLogic { const provider = this.fileManager.fileProviderOf(this.currentPath) if (!provider) return cb(null, []) const tests = [] - let files + let files = [] try { - files = await this.fileManager.readdir(this.currentPath) + if (await this.fileManager.exists(this.currentPath)) files = await this.fileManager.readdir(this.currentPath) } catch (e) { cb(e.message) } diff --git a/apps/remix-ide/src/app/ui/landing-page/landing-page.js b/apps/remix-ide/src/app/ui/landing-page/landing-page.js index e35de92041..bf89d166d8 100644 --- a/apps/remix-ide/src/app/ui/landing-page/landing-page.js +++ b/apps/remix-ide/src/app/ui/landing-page/landing-page.js @@ -240,17 +240,22 @@ export class LandingPage extends ViewPlugin {
e.g ${examples.map((url) => { return yo`` })}
` - modalDialogCustom.prompt(`Import from ${service}`, msg, null, (target) => { + const title = `Import from ${service}` + modalDialogCustom.prompt(title, msg, null, (target) => { if (target !== '') { compilerImport.import( target, (loadingMsg) => { tooltip(loadingMsg) }, (error, content, cleanUrl, type, url) => { if (error) { - modalDialogCustom.alert(error) + modalDialogCustom.alert(title, error.message || error) } else { - fileProviders.browser.addExternal(type + '/' + cleanUrl, content, url) - this.verticalIcons.select('fileExplorers') + try { + fileProviders.workspace.addExternal(type + '/' + cleanUrl, content, url) + this.verticalIcons.select('fileExplorers') + } catch (e) { + modalDialogCustom.alert(title, e.message) + } } } ) diff --git a/apps/remix-ide/src/app/ui/modal-dialog-custom.js b/apps/remix-ide/src/app/ui/modal-dialog-custom.js index bcbd4aaec5..d2b1daba44 100644 --- a/apps/remix-ide/src/app/ui/modal-dialog-custom.js +++ b/apps/remix-ide/src/app/ui/modal-dialog-custom.js @@ -5,7 +5,7 @@ var css = require('./styles/modal-dialog-custom-styles') module.exports = { alert: function (title, text) { if (text) return modal(title, yo`
${text}
`, null, { label: null }) - return modal('', yo`
${title}
`, null, { label: null }) + return modal('Alert', yo`
${title}
`, null, { label: null }) }, prompt: function (title, text, inputValue, ok, cancel, focus) { return prompt(title, text, false, inputValue, ok, cancel, focus) diff --git a/apps/remix-ide/src/lib/gist-handler.js b/apps/remix-ide/src/lib/gist-handler.js index d046befd24..92c170b10f 100644 --- a/apps/remix-ide/src/lib/gist-handler.js +++ b/apps/remix-ide/src/lib/gist-handler.js @@ -20,7 +20,7 @@ function GistHandler (_window) { if (gistId) { cb(gistId) } else { - modalDialogCustom.alert('Error while loading gist. Please provide a valid Gist ID or URL.') + modalDialogCustom.alert('Gist load error', 'Error while loading gist. Please provide a valid Gist ID or URL.') } } }) @@ -49,7 +49,7 @@ function GistHandler (_window) { json: true }, async (error, response, data = {}) => { if (error || !data.files) { - modalDialogCustom.alert(`Gist load error: ${error || data.message}`) + modalDialogCustom.alert('Gist load error', error || data.message) return } const obj = {} @@ -60,6 +60,8 @@ function GistHandler (_window) { if (!errorLoadingFile) { const provider = fileManager.getProvider('workspace') provider.lastLoadedGistId = gistId + } else { + modalDialogCustom.alert('Gist load error', errorLoadingFile.message || errorLoadingFile) } }) }) diff --git a/apps/remix-ide/src/lib/helper.js b/apps/remix-ide/src/lib/helper.js index a96b8b0256..ac3f71a0e5 100644 --- a/apps/remix-ide/src/lib/helper.js +++ b/apps/remix-ide/src/lib/helper.js @@ -55,6 +55,9 @@ module.exports = { checkSpecialChars (name) { return name.match(/[:*?"<>\\'|]/) != null }, + checkSlash (name) { + return name.match(/\//) != null + }, isHexadecimal (value) { return /^[0-9a-fA-F]+$/.test(value) && (value.length % 2 === 0) }, diff --git a/apps/remix-ide/src/migrateFileSystem.js b/apps/remix-ide/src/migrateFileSystem.js index f626e1c20b..0a64ba5b29 100644 --- a/apps/remix-ide/src/migrateFileSystem.js +++ b/apps/remix-ide/src/migrateFileSystem.js @@ -29,17 +29,20 @@ export async function migrateToWorkspace (fileManager, filePanel) { if (fileStorageBrowserWorkspace.get(flag) === 'done') return const files = await browserProvider.copyFolderToJson('/') console.log(files) - const workspaceName = 'default_workspace' - const workspacePath = joinPath('browser', workspaceProvider.workspacesPath, workspaceName) - await filePanel.createWorkspace(workspaceName) - filePanel.getWorkspaces() // refresh list - await populateWorkspace(workspacePath, files, browserProvider) + if (Object.keys(files).length > 0) { + const workspaceName = 'default_workspace' + const workspacePath = joinPath('browser', workspaceProvider.workspacesPath, workspaceName) + await filePanel.processCreateWorkspace(workspaceName) + filePanel.getWorkspaces() // refresh list + await populateWorkspace(workspacePath, files, browserProvider) + } fileStorageBrowserWorkspace.set(flag, 'done') } const populateWorkspace = async (workspace, json, browserProvider) => { for (const item in json) { const isFolder = json[item].content === undefined + if (isFolder && item === '/.workspaces') continue // we don't want to replicate this one. if (isFolder) { browserProvider.createDir(joinPath(workspace, item)) await populateWorkspace(workspace, json[item].children, browserProvider) diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx index 19bb68b89f..eabf61d29e 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx @@ -16,12 +16,12 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => { }, { action: 'publishToGist', - title: 'Publish all [browser] explorer files to a github gist', + title: 'Publish all the current workspace files (only root) to a github gist', icon: 'fab fa-github' }, { action: 'uploadFile', - title: 'Load a local file into Remix\'s browser folder', + title: 'Load a local file into current workspace', icon: 'fa fa-upload' }, { diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index d97990e788..00ae818a0f 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -7,6 +7,7 @@ import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line export interface WorkspaceProps { setWorkspace: ({ name: string, isLocalhost: boolean }) => void, createWorkspace: (name: string) => void, + renameWorkspace: (oldName: string, newName: string) => void workspaceRenamed: ({ name: string }) => void, workspaceCreated: ({ name: string }) => void, workspaceDeleted: ({ name: string }) => void, @@ -28,9 +29,9 @@ export const Workspace = (props: WorkspaceProps) => { const NO_WORKSPACE = ' - none - ' /* extends the parent 'plugin' with some function needed by the file explorer */ - props.plugin.resetFocus = () => { + props.plugin.resetFocus = (reset) => { setState(prevState => { - return { ...prevState, reset: true } + return { ...prevState, reset } }) } @@ -40,6 +41,8 @@ export const Workspace = (props: WorkspaceProps) => { }) } + props.plugin.resetUploadFile = () => {} + /* implement an external API, consumed by the parent */ props.request.createWorkspace = () => { return createWorkspace() @@ -142,7 +145,7 @@ export const Workspace = (props: WorkspaceProps) => { } const deleteCurrentWorkspace = () => { - modal('Remove Workspace', 'Please choose a name for the workspace', { + modal('Remove Workspace', 'Are you sure to delete the current workspace?', { label: 'OK', fn: onFinishDeleteWorkspace }, { @@ -152,13 +155,12 @@ export const Workspace = (props: WorkspaceProps) => { } const modalMessage = (title: string, body: string) => { - modal(title, body, { - label: 'OK', - fn: () => {} - }, { - label: null, - fn: null - }) + setTimeout(() => { // wait for any previous modal a chance to close + modal(title, body, { + label: 'OK', + fn: () => {} + }, null) + }, 200) } const workspaceRenameInput = useRef() @@ -168,10 +170,15 @@ export const Workspace = (props: WorkspaceProps) => { 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 }) + + try { + await props.renameWorkspace(state.currentWorkspace, workspaceName) + setWorkspace(workspaceName) + props.workspaceRenamed({ name: workspaceName }) + } catch (e) { + modalMessage('Rename Workspace', e.message) + console.error(e) + } } const onFinishCreateWorkspace = async () => { @@ -181,11 +188,11 @@ export const Workspace = (props: WorkspaceProps) => { try { await props.createWorkspace(workspaceName) + await setWorkspace(workspaceName) } catch (e) { - modalMessage('Workspace Creation', e.message) + modalMessage('Create Workspace', e.message) console.error(e) } - await setWorkspace(workspaceName) } const onFinishDeleteWorkspace = async () => { @@ -261,7 +268,7 @@ export const Workspace = (props: WorkspaceProps) => { return ( <> { state.modal.message } - + ) } @@ -270,7 +277,7 @@ export const Workspace = (props: WorkspaceProps) => { return ( <> { state.modal.message } - + ) }