Merge pull request #2353 from ethereum/wsTemplates

create workspace using templates
pull/2374/head
yann300 3 years ago committed by GitHub
commit 31983489b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 76
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  2. 4
      apps/remix-ide/src/app/panels/file-panel.js
  3. 6
      libs/remix-ui/workspace/src/lib/actions/events.ts
  4. 4
      libs/remix-ui/workspace/src/lib/actions/index.ts
  5. 38
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  6. 2
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  7. 6
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  8. 14
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  9. 1
      libs/remix-ui/workspace/src/lib/templates/blank.ts
  10. 125
      libs/remix-ui/workspace/src/lib/templates/erc20.ts
  11. 3
      libs/remix-ui/workspace/src/lib/templates/index.ts
  12. 2
      libs/remix-ui/workspace/src/lib/templates/remixDefault.ts
  13. 3
      libs/remix-ui/workspace/src/lib/types/index.ts

@ -31,9 +31,83 @@ module.exports = {
.waitForElementNotPresent('[data-id="landingPageHomeContainer"]')
},
'Should create two workspace and switch to the first one #group1': function (browser: NightwatchBrowser) {
// WORKSPACE TEMPLATES E2E START
'Should create Remix default workspace with files': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.click('*[data-id="workspaceCreate"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_remix_default' })
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(1000)
.assert.elementPresent('*[data-id="treeViewLitreeViewItemcontracts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/deploy_with_web3.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/deploy_with_ethers.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/web3.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/ethers.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemtests"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemtests/storage.test.js"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemtests/Ballot_test.sol"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemREADME.txt"]')
},
'Should create blank workspace with no files': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="workspaceCreate"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_blank' })
.click('select[id="wstemplate"]')
.click('select[id="wstemplate"] option[value=blank]')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(1000)
.assert.elementPresent('*[data-id="treeViewUltreeViewMenu"]')
.execute(function () {
const fileList = document.querySelector('*[data-id="treeViewUltreeViewMenu"]')
return fileList.getElementsByTagName('li').length;
}, [], function(result){
// check there are no files in FE
browser.assert.equal(result.value, 0, 'Incorrect number of files');
});
},
'Should create ERC20 workspace with files': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="workspaceCreate"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_erc20' })
.click('select[id="wstemplate"]')
.click('select[id="wstemplate"] option[value=erc20]')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(1000)
.assert.elementPresent('*[data-id="treeViewLitreeViewItemcontracts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemcontracts/SampleERC20.sol"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/deploy_with_web3.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/deploy_with_ethers.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/web3.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemscripts/ethers.ts"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemtests"]')
.assert.elementPresent('*[data-id="treeViewLitreeViewItemtests/SampleERC20_test.sol"]')
},
// WORKSPACE TEMPLATES E2E END
'Should create two workspace and switch to the first one': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="workspaceCreate"]') // create workspace_name
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')

@ -121,9 +121,9 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
createWorkspace (workspaceName, isEmpty) {
createWorkspace (workspaceName, workspaceTemplateName, isEmpty) {
return new Promise((resolve, reject) => {
this.emit('createWorkspaceReducerEvent', workspaceName, isEmpty, (err, data) => {
this.emit('createWorkspaceReducerEvent', workspaceName, workspaceTemplateName, isEmpty, (err, data) => {
if (err) reject(err)
else resolve(data || true)
})

@ -1,6 +1,6 @@
import { extractParentFromKey } from '@remix-ui/helper'
import React from 'react'
import { action } from '../types'
import { action, WorkspaceTemplate } from '../types'
import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, loadLocalhostError, loadLocalhostRequest, loadLocalhostSuccess, removeContextMenuItem, removeFocus, rootFolderChangedSuccess, setContextMenuItem, setMode, setReadOnlyMode } from './payload'
import { addInputField, createWorkspace, deleteWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from './workspace'
@ -10,8 +10,8 @@ let plugin, dispatch: React.Dispatch<any>
export const listenOnPluginEvents = (filePanelPlugin) => {
plugin = filePanelPlugin
plugin.on('filePanel', 'createWorkspaceReducerEvent', (name: string, isEmpty = false, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
createWorkspace(name, isEmpty, cb)
plugin.on('filePanel', 'createWorkspaceReducerEvent', (name: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
createWorkspace(name, workspaceTemplateName, isEmpty, cb)
})
plugin.on('filePanel', 'renameWorkspaceReducerEvent', (oldName: string, workspaceName: string, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {

@ -46,10 +46,10 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath))
} else {
if (workspaces.length === 0) {
await createWorkspaceTemplate('default_workspace', 'default-template')
await createWorkspaceTemplate('default_workspace', 'remixDefault')
plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false })
dispatch(setCurrentWorkspace('default_workspace'))
await loadWorkspacePreset('default-template')
await loadWorkspacePreset('remixDefault')
} else {
if (workspaces.length > 0) {
workspaceProvider.setWorkspace(workspaces[workspaces.length - 1])

@ -4,8 +4,7 @@ import axios, { AxiosResponse } from 'axios'
import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload'
import { checkSlash, checkSpecialChars } from '@remix-ui/helper'
import { JSONStandardInput } from '../types'
import { examples } from '../templates/examples'
import { JSONStandardInput, WorkspaceTemplate } from '../types'
import { QueryParams } from '@remix-project/remix-lib'
@ -41,9 +40,9 @@ export const addInputField = async (type: 'file' | 'folder', path: string, cb?:
return promise
}
export const createWorkspace = async (workspaceName: string, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, 'default-template')
const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName)
dispatch(createWorkspaceRequest(promise))
promise.then(async () => {
@ -51,7 +50,7 @@ export const createWorkspace = async (workspaceName: string, isEmpty = false, cb
await plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
await plugin.setWorkspaces(await getWorkspaces())
await plugin.workspaceCreated(workspaceName)
if (!isEmpty) await loadWorkspacePreset('default-template')
if (!isEmpty) await loadWorkspacePreset(workspaceTemplateName)
cb && cb(null, workspaceName)
}).catch((error) => {
dispatch(createWorkspaceError({ error }))
@ -60,10 +59,10 @@ export const createWorkspace = async (workspaceName: string, isEmpty = false, cb
return promise
}
export const createWorkspaceTemplate = async (workspaceName: string, template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => {
export const createWorkspaceTemplate = async (workspaceName: string, template: WorkspaceTemplate = 'remixDefault') => {
if (!workspaceName) throw new Error('workspace name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await workspaceExists(workspaceName) && template === 'default-template') throw new Error('workspace already exists')
if (await workspaceExists(workspaceName) && template === 'remixDefault') throw new Error('workspace already exists')
else {
const workspaceProvider = plugin.fileProviders.workspace
@ -77,7 +76,7 @@ export type UrlParametersType = {
url: string
}
export const loadWorkspacePreset = async (template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => {
export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDefault') => {
const workspaceProvider = plugin.fileProviders.workspace
const params = queryParams.get() as UrlParametersType
@ -150,15 +149,22 @@ export const loadWorkspacePreset = async (template: 'gist-template' | 'code-temp
}
break
case 'default-template':
// creates a new workspace and populates it with default project template.
// insert example contracts
for (const file in examples) {
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
default:
try {
const templateWithContent = await import('../templates')
const templateList = Object.keys(templateWithContent)
if (!templateList.includes(template)) break
const files = templateWithContent[template]
for (const file in files) {
try {
await workspaceProvider.set(files[file].name, files[file].content)
} catch (error) {
console.error(error)
}
}
} catch (e) {
dispatch(displayNotification('Workspace load error', e.message, 'OK', null, () => { dispatch(hideNotification()) }, null))
console.error(e)
}
break
}

@ -9,7 +9,7 @@ export const FileSystemContext = createContext<{
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchCreateWorkspace: (workspaceName: string) => Promise<void>,
dispatchCreateWorkspace: (workspaceName: string, workspaceTemplateName: string) => Promise<void>,
toast: (toasterMsg: string) => void,
dispatchFetchWorkspaceDirectory: (path: string) => Promise<void>,
dispatchSwitchToWorkspace: (name: string) => Promise<void>,

@ -6,7 +6,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip } from '../actions'
import { Modal, WorkspaceProps } from '../types'
import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type'
@ -43,8 +43,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await removeInputField(path)
}
const dispatchCreateWorkspace = async (workspaceName: string) => {
await createWorkspace(workspaceName)
const dispatchCreateWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate) => {
await createWorkspace(workspaceName, workspaceTemplateName)
}
const dispatchFetchWorkspaceDirectory = async (path: string) => {

@ -12,6 +12,7 @@ export function Workspace () {
const global = useContext(FileSystemContext)
const workspaceRenameInput = useRef()
const workspaceCreateInput = useRef()
const workspaceCreateTemplateInput = useRef()
useEffect(() => {
resetFocus()
@ -83,9 +84,11 @@ export function Workspace () {
if (workspaceCreateInput.current === undefined) return
// @ts-ignore: Object is possibly 'null'.
const workspaceName = workspaceCreateInput.current.value
// @ts-ignore: Object is possibly 'null'.
const workspaceTemplateName = workspaceCreateTemplateInput.current.value || 'remixDefault'
try {
await global.dispatchCreateWorkspace(workspaceName)
await global.dispatchCreateWorkspace(workspaceName, workspaceTemplateName)
} catch (e) {
global.modal('Create Workspace', e.message, 'OK', () => {}, '')
console.error(e)
@ -119,7 +122,14 @@ export function Workspace () {
const createModalMessage = () => {
return (
<>
<input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" />
<label id="wsName" className="form-check-label">Workspace name</label>
<input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" /><br/>
<label id="selectWsTemplate" className="form-check-label">Choose a template</label>
<select name="wstemplate" className="form-control custom-select" id="wstemplate" defaultValue='remixDefault' ref={workspaceCreateTemplateInput}>
<option value='remixDefault'>Default</option>
<option value='blank'>Blank</option>
<option value='erc20'>ERC20</option>
</select>
</>
)
}

@ -0,0 +1,125 @@
'use strict'
const erc20 = `// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title SampleERC20
* @dev Create a sample ERC20 standard token
*/
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract SampleERC20 is ERC20 {
constructor(string memory tokenName, string memory tokenSymbol) ERC20(tokenName, tokenSymbol) {}
}`
const erc20_test = `// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "remix_tests.sol";
import "../contracts/SampleERC20.sol";
contract SampleERC20Test {
SampleERC20 s;
function beforeAll () public {
s = new SampleERC20("TestToken", "TST");
}
function testTokenNameAndSymbol () public {
Assert.equal(s.name(), "TestToken", "token name did not match");
Assert.equal(s.symbol(), "TST", "token symbol did not match");
}
}
`
/* eslint-disable no-useless-escape */
const deployWithWeb3 = `import { deploy } from './web3.ts'
(async () => {
try {
const result = await deploy('SampleERC20', ['testToken', 'TST'])
console.log(\`address: \${result.address\}\`)
} catch (e) {
console.log(e.message)
}
})()`
const deployWithEthers = `import { deploy } from './ethers.ts'
(async () => {
try {
const result = await deploy('SampleERC20', ['testToken', 'TST'])
console.log(\`address: \${result.address\}\`)
} catch (e) {
console.log(e.message)
}
})()`
const libWeb3 = `
export const deploy = async (contractName: string, args: Array<any>, from?: string, gas?: number) => {
console.log(\`deploying \${contractName\}\`)
// Note that the script needs the ABI which is generated from the compilation artifact.
// Make sure contract is compiled and artifacts are generated
const artifactsPath = \`browser/contracts/artifacts/\${contractName\}.json\`
const metadata = JSON.parse(await remix.call('fileManager', 'getFile', artifactsPath))
const accounts = await web3.eth.getAccounts()
let contract = new web3.eth.Contract(metadata.abi)
contract = contract.deploy({
data: metadata.data.bytecode.object,
arguments: args
})
const newContractInstance = await contract.send({
from: from || accounts[0],
gas: gas || 1500000
})
return newContractInstance.options
}: Promise<any>`
const libEthers = `
export const deploy = async (contractName: string, args: Array<any>, from?: string) => {
console.log(\`deploying \${contractName\}\`)
// Note that the script needs the ABI which is generated from the compilation artifact.
// Make sure contract is compiled and artifacts are generated
const artifactsPath = \`browser/contracts/artifacts/\${contractName\}.json\`
const metadata = JSON.parse(await remix.call('fileManager', 'getFile', artifactsPath))
// 'web3Provider' is a remix global variable object
const signer = (new ethers.providers.Web3Provider(web3Provider)).getSigner()
let factory = new ethers.ContractFactory(metadata.abi, metadata.data.bytecode.object, signer);
let contract
if (from) {
contract = await factory.connect(from).deploy(...args);
} else {
contract = await factory.deploy(...args);
}
// The contract is NOT deployed yet; we must wait until it is mined
await contract.deployed()
return contract
}: Promise<any>`
/* eslint-enable no-useless-escape */
export default {
erc20: { name: 'contracts/SampleERC20.sol', content: erc20 },
erc20_test: { name: 'tests/SampleERC20_test.sol', content: erc20_test },
deployWithWeb3: { name: 'scripts/deploy_with_web3.ts', content: deployWithWeb3 },
deployWithEthers: { name: 'scripts/deploy_with_ethers.ts', content: deployWithEthers },
web3: { name: 'scripts/web3.ts', content: libWeb3 },
ethers: { name: 'scripts/ethers.ts', content: libEthers },
}

@ -0,0 +1,3 @@
export { default as remixDefault } from './remixDefault'
export { default as erc20 } from './erc20'
export { default as blank } from './blank'

@ -392,7 +392,7 @@ For now, modules supported by Remix are ethers, web3, swarmgw, chai, remix and h
For unsupported modules, an error like this will be thrown: '<module_name> module require is not supported by Remix IDE will be shown.'
`
export const examples = {
export default {
storage: { name: 'contracts/1_Storage.sol', content: storage },
owner: { name: 'contracts/2_Owner.sol', content: owner },
ballot: { name: 'contracts/3_Ballot.sol', content: ballot },

@ -13,10 +13,11 @@ export interface JSONStandardInput {
};
}
export type MenuItems = action[]
export type WorkspaceTemplate = 'gist-template' | 'code-template' | 'remixDefault' | 'blank' | 'erc20'
export interface WorkspaceProps {
plugin: {
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void,
createWorkspace: (name: string) => void,
createWorkspace: (name: string, workspaceTemplateName: string) => void,
renameWorkspace: (oldName: string, newName: string) => void
workspaceRenamed: ({ name: string }) => void,
workspaceCreated: ({ name: string }) => void,

Loading…
Cancel
Save