Merge pull request #933 from ethereum/throwIfNoWorkspace

Throw if no workspace
pull/940/head
yann300 4 years ago committed by GitHub
commit d56bf6d489
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/remix-ide-e2e/src/tests/gist.test.ts
  2. 1
      apps/remix-ide-e2e/src/tests/solidityImport.test.ts
  3. 2
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  4. 23
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  5. 14
      apps/remix-ide/src/app/files/fileManager.js
  6. 12
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  7. 17
      apps/remix-ide/src/app/panels/file-panel.js
  8. 3
      apps/remix-ide/src/app/tabs/styles/test-tab-styles.js
  9. 17
      apps/remix-ide/src/app/tabs/test-tab.js
  10. 7
      apps/remix-ide/src/app/tabs/testTab/testTab.js
  11. 13
      apps/remix-ide/src/app/ui/landing-page/landing-page.js
  12. 2
      apps/remix-ide/src/app/ui/modal-dialog-custom.js
  13. 6
      apps/remix-ide/src/lib/gist-handler.js
  14. 3
      apps/remix-ide/src/lib/helper.js
  15. 13
      apps/remix-ide/src/migrateFileSystem.js
  16. 4
      libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx
  17. 43
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx

@ -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()
},

@ -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) {

@ -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) {

@ -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()
},

@ -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()

@ -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 + '/', '')
}
}

@ -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(
<Workspace
createWorkspace={this.createWorkspace.bind(this)}
renameWorkspace={this.renameWorkspace.bind(this)}
setWorkspace={this.setWorkspace.bind(this)}
workspaceRenamed={this.workspaceRenamed.bind(this)}
workspaceDeleted={this.workspaceDeleted.bind(this)}
@ -165,8 +167,8 @@ module.exports = class Filepanel extends ViewPlugin {
return await this.request.createNewFile()
}
async uploadFile () {
return await this.request.uploadFile()
async uploadFile (event) {
return await this.request.uploadFile(event)
}
async processCreateWorkspace (name) {
@ -186,6 +188,8 @@ module.exports = class Filepanel extends ViewPlugin {
}
async createWorkspace (workspaceName) {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
const browserProvider = this._deps.fileProviders.browser
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
@ -199,6 +203,15 @@ module.exports = class Filepanel extends ViewPlugin {
}
}
async renameWorkspace (oldName, workspaceName) {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
const browserProvider = this._deps.fileProviders.browser
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
browserProvider.rename('browser/' + workspacesPath + '/' + oldName, 'browser/' + workspacesPath + '/' + workspaceName, true)
}
/** 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)

@ -48,5 +48,8 @@ var css = csjs`
.labelOnBtn {
border: hidden;
}
.inputFolder {
width: 80%;
}
`
module.exports = css

@ -573,7 +573,7 @@ module.exports = class TestTab extends ViewPlugin {
this.inputPath = yo`<input
placeholder=${this.defaultPath}
list="utPathList"
class="custom-select"
class="${css.inputFolder} custom-select"
id="utPath"
data-id="uiPathInput"
name="utPath"
@ -581,10 +581,21 @@ module.exports = class TestTab extends ViewPlugin {
onkeydown=${(e) => { if (e.keyCode === 191) this.updateDirList() }}
onchange=${(e) => this.updateCurrentPath(e)}/>`
const createTestFolder = yo`<button
class="btn border ml-2"
data-id="testTabGenerateTestFolder"
title="Create a test folder"
onclick=${(e) => { this.testTabLogic.generateTestFolder(this.inputPath.value) }}>
Create
</button>`
const availablePaths = yo`
<div>
${this.inputPath}
${this.uiPathList}
<div class="d-flex p-2">
${this.inputPath}
${createTestFolder}
${this.uiPathList}
</div>
</div>
`
this.updateDirList()

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

@ -240,17 +240,22 @@ export class LandingPage extends ViewPlugin {
<div>e.g ${examples.map((url) => { return yo`<div class="p-1"><a>${url}</a></div>` })}</div>
</div>`
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)
}
}
}
)

@ -5,7 +5,7 @@ var css = require('./styles/modal-dialog-custom-styles')
module.exports = {
alert: function (title, text) {
if (text) return modal(title, yo`<div>${text}</div>`, null, { label: null })
return modal('', yo`<div>${title}</div>`, null, { label: null })
return modal('Alert', yo`<div>${title}</div>`, null, { label: null })
},
prompt: function (title, text, inputValue, ok, cancel, focus) {
return prompt(title, text, false, inputValue, ok, cancel, focus)

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

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

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

@ -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'
},
{

@ -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 (
<>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextCreate" placeholder={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" />
<input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" />
</>
)
}
@ -270,7 +277,7 @@ export const Workspace = (props: WorkspaceProps) => {
return (
<>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextRename" placeholder={ state.currentWorkspace } ref={workspaceRenameInput} className="form-control" />
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ state.currentWorkspace } ref={workspaceRenameInput} className="form-control" />
</>
)
}

Loading…
Cancel
Save