Merge pull request #3654 from ethereum/startCoding

recent workspaces & Start Coding
pull/5370/head
yann300 1 year ago committed by GitHub
commit ac97639619
  1. 23
      apps/remix-ide-e2e/src/tests/homeTab.test.ts
  2. 4
      apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
  3. 33
      apps/remix-ide/src/app/files/fileManager.ts
  4. 95
      apps/remix-ide/src/app/panels/file-panel.js
  5. 3
      apps/remix-ide/src/app/tabs/locales/en/home.json
  6. 1
      apps/remix-ide/src/app/tabs/locales/fr/home.json
  7. 6
      apps/remix-ide/src/remixAppManager.js
  8. 220
      libs/remix-ui/home-tab/src/lib/components/homeTabFile.tsx
  9. 22
      libs/remix-ui/home-tab/src/lib/components/homeTabGetStarted.tsx
  10. 6
      libs/remix-ui/plugin-manager/src/lib/components/LocalPluginForm.tsx
  11. 4
      libs/remix-ui/workspace/src/lib/actions/events.ts
  12. 411
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  13. 6
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  14. 148
      libs/remix-ui/workspace/src/lib/types/index.ts
  15. 1
      libs/remix-ui/workspace/src/lib/utils/constants.ts
  16. 1
      libs/remix-ws-templates/src/index.ts
  17. 38
      libs/remix-ws-templates/src/templates/playground/.prettierrc
  18. 28
      libs/remix-ws-templates/src/templates/playground/README.txt
  19. 8
      libs/remix-ws-templates/src/templates/playground/contracts/helloWorld.sol
  20. 16
      libs/remix-ws-templates/src/templates/playground/index.ts
  21. 14
      libs/remix-ws-templates/src/templates/playground/scripts/deploy_with_ethers.ts
  22. 14
      libs/remix-ws-templates/src/templates/playground/scripts/deploy_with_web3.ts
  23. 29
      libs/remix-ws-templates/src/templates/playground/scripts/ethers-lib.ts
  24. 36
      libs/remix-ws-templates/src/templates/playground/scripts/web3-lib.ts

@ -4,17 +4,14 @@ import init from '../helpers/init'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
'Should create new file': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="homeTabNewFile"]')
.click('*[data-id="homeTabNewFile"]')
.waitForElementContainsText('*[data-id$="/blank"]', '', 60000)
.sendKeys('*[data-id$="/blank"] .remixui_items', 'newTestFile')
.sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('li[data-id="treeViewLitreeViewItemnewTestFile.sol"]')
}
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
'Should start coding': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="homeTabStartCoding"]')
.click('*[data-id="homeTabStartCoding"]')
.waitForElementVisible('div[data-id="treeViewDivtreeViewItemcontracts/helloWorld.sol"]')
}
}

@ -235,7 +235,7 @@ const sources = [
content:
`
pragma solidity ^0.8.0;
contract helloWorld {
contract HelloWorld {
string public message;
fallback () external {
@ -249,7 +249,7 @@ const sources = [
},
'checkBalance.sol': {
content: `pragma solidity ^0.8.0;
contract checkBalance {
contract CheckBalance {
constructor () payable {}
function sendSomeEther(uint256 num) public {

@ -22,9 +22,11 @@ const profile = {
icon: 'assets/img/fileManager.webp',
permission: true,
version: packageJson.version,
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'writeMultipleFiles', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir',
'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh',
'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles', 'isGitRepo', 'isFile', 'isDirectory'],
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'writeMultipleFiles', 'writeFileNoRewrite',
'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile',
'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath',
'saveCurrentFile', 'setBatchFiles', 'isGitRepo', 'isFile', 'isDirectory'
],
kind: 'file-system'
}
const errorMsg = {
@ -37,7 +39,6 @@ const errorMsg = {
const createError = (err) => {
return new Error(`${errorMsg[err.code]} ${err.message || ''}`)
}
class FileManager extends Plugin {
mode: string
openedFiles: any
@ -249,6 +250,30 @@ class FileManager extends Plugin {
throw new Error(e)
}
}
/**
* Set the content of a specific file, does nnot rewrite file if it exists but creates a new unique name
* @param {string} path path of the file
* @param {string} data content to write on the file
* @returns {void}
*/
async writeFileNoRewrite(path, data) {
try {
path = this.normalize(path)
path = this.limitPluginScope(path)
if (await this.exists(path)) {
const newPath = await helper.createNonClashingNameAsync(path, this)
const content = await this.setFileContent(newPath, data)
return {newContent: content, newPath}
} else {
const ret = await this.setFileContent(path, data)
this.emit('fileAdded', path)
return {newContent: ret, newpath: path}
}
} catch (e) {
throw new Error(e)
}
}
/**
* Return the content of a specific file

@ -30,7 +30,19 @@ const { SlitherHandle } = require('../files/slither-handle.js')
const profile = {
name: 'filePanel',
displayName: 'File explorer',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getAvailableWorkspaceName', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'],
methods: [
'createNewFile',
'uploadFile',
'getCurrentWorkspace',
'getAvailableWorkspaceName',
'getWorkspaces',
'createWorkspace',
'switchToWorkspace',
'setWorkspace',
'registerContextMenuItem',
'renameWorkspace',
'deleteWorkspace',
],
events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp',
description: 'Remix IDE file explorer',
@ -41,7 +53,7 @@ const profile = {
maintainedBy: 'Remix'
}
module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) {
constructor(appManager) {
super(profile)
this.registry = Registry.getInstance()
this.fileProviders = this.registry.get('fileproviders').api
@ -60,13 +72,17 @@ module.exports = class Filepanel extends ViewPlugin {
this.currentWorkspaceMetadata = null
}
render () {
return <div id='fileExplorerView'><FileSystemProvider plugin={this} /></div>
render() {
return (
<div id="fileExplorerView">
<FileSystemProvider plugin={this} />
</div>
)
}
/**
* @param item { id: string, name: string, type?: string[], path?: string[], extension?: string[], pattern?: string[] }
* typically:
* typically:
* group 0 for file manipulations
* group 1 for download operations
* group 2 for running operations (script for instance)
@ -77,7 +93,7 @@ module.exports = class Filepanel extends ViewPlugin {
* group 7 for generating resource files (UML, documentation, ...)
* @param callback (...args) => void
*/
registerContextMenuItem (item) {
registerContextMenuItem(item) {
return new Promise((resolve, reject) => {
this.emit('registerContextMenuItemReducerEvent', item, (err, data) => {
if (err) reject(err)
@ -86,7 +102,7 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
removePluginActions (plugin) {
removePluginActions(plugin) {
return new Promise((resolve, reject) => {
this.emit('removePluginActionsReducerEvent', plugin, (err, data) => {
if (err) reject(err)
@ -95,30 +111,30 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
getCurrentWorkspace () {
getCurrentWorkspace() {
return this.currentWorkspaceMetadata
}
getWorkspaces () {
getWorkspaces() {
return this.workspaces
}
getAvailableWorkspaceName (name) {
if(!this.workspaces) return name
getAvailableWorkspaceName(name) {
if (!this.workspaces) return name
let index = 1
let workspace = this.workspaces.find(workspace => workspace.name === name + ' - ' + index)
let workspace = this.workspaces.find((workspace) => workspace.name === name + ' - ' + index)
while (workspace) {
index++
workspace = this.workspaces.find(workspace => workspace.name === name + ' - ' + index)
workspace = this.workspaces.find((workspace) => workspace.name === name + ' - ' + index)
}
return name + ' - ' + index
}
setWorkspaces (workspaces) {
setWorkspaces(workspaces) {
this.workspaces = workspaces
}
createNewFile () {
createNewFile() {
return new Promise((resolve, reject) => {
this.emit('createNewFileInputReducerEvent', '/', (err, data) => {
if (err) reject(err)
@ -127,7 +143,7 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
uploadFile (target) {
uploadFile(target) {
return new Promise((resolve, reject) => {
return this.emit('uploadFileReducerEvent', '/', target, (err, data) => {
if (err) reject(err)
@ -136,7 +152,7 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
createWorkspace (workspaceName, workspaceTemplateName, isEmpty) {
createWorkspace(workspaceName, workspaceTemplateName, isEmpty) {
return new Promise((resolve, reject) => {
this.emit('createWorkspaceReducerEvent', workspaceName, workspaceTemplateName, isEmpty, (err, data) => {
if (err) reject(err)
@ -145,7 +161,7 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
renameWorkspace (oldName, workspaceName) {
renameWorkspace(oldName, workspaceName) {
return new Promise((resolve, reject) => {
this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => {
if (err) reject(err)
@ -154,7 +170,7 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
deleteWorkspace (workspaceName) {
deleteWorkspace(workspaceName) {
return new Promise((resolve, reject) => {
this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => {
if (err) reject(err)
@ -163,25 +179,52 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
setWorkspace (workspace) {
const workspaceProvider = this.fileProviders.workspace
saveRecent(workspaceName) {
if (!localStorage.getItem('recentWorkspaces')) {
localStorage.setItem('recentWorkspaces', JSON.stringify([ workspaceName ]))
} else {
let recents = JSON.parse(localStorage.getItem('recentWorkspaces'))
// checking if we have a duplication
if (!recents.find((el) => {
return el === workspaceName
})) {
recents = ([workspaceName, ...recents])
recents = recents.filter((el) => { return el != "" })
localStorage.setItem('recentWorkspaces', JSON.stringify(recents))
}
}
}
this.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` }
if (workspace.name !== " - connect to localhost - ") {
setWorkspace(workspace) {
const workspaceProvider = this.fileProviders.workspace
const current = this.currentWorkspaceMetadata
this.currentWorkspaceMetadata = {
name: workspace.name,
isLocalhost: workspace.isLocalhost,
absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}`,
}
if (this.currentWorkspaceMetadata.name !== current) {
this.saveRecent(workspace.name)
}
if (workspace.name !== ' - connect to localhost - ') {
localStorage.setItem('currentWorkspace', workspace.name)
}
this.emit('setWorkspace', workspace)
}
workspaceRenamed (oldName, workspaceName) {
switchToWorkspace(workspaceName) {
this.emit('switchToWorkspace', workspaceName)
}
workspaceRenamed(oldName, workspaceName) {
this.emit('workspaceRenamed', oldName, workspaceName)
}
workspaceDeleted (workspace) {
workspaceDeleted(workspace) {
this.emit('workspaceDeleted', workspace)
}
workspaceCreated (workspace) {
workspaceCreated(workspace) {
this.emit('workspaceCreated', workspace)
}
/** end section */

@ -56,7 +56,10 @@
"home.searchDocumentation": "Search Documentation",
"home.files": "Files",
"home.newFile": "New File",
"home.startCoding": "Start Coding",
"home.startCodingPlayground": "Open a playground for prototyping and simply learning",
"home.openFile": "Open File",
"home.openFileTooltip": "Open a File from your File System",
"home.accessFileSystem": "Access File System",
"home.loadFrom": "Load from",
"home.resources": "Resources",

@ -55,6 +55,7 @@
"home.files": "Files",
"home.newFile": "New File",
"home.openFile": "Open File",
"home.openFileTooltip": "Open a File from you File System",
"home.connectToLocalhost": "Access File System",
"home.loadFrom": "Load from",
"home.resources": "Resources"

@ -5,8 +5,7 @@ import {IframePlugin} from '@remixproject/engine-web'
const _paq = (window._paq = window._paq || [])
// requiredModule removes the plugin from the plugin manager list on UI
const requiredModules = [
// services + layout views + system views
const requiredModules = [ // services + layout views + system views
'manager',
'config',
'compilerArtefacts',
@ -74,7 +73,8 @@ const requiredModules = [
'compilationDetails',
'contractflattener',
'solidity-script',
'openaigpt'
'openaigpt',
'home'
]
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, {useState, useRef, useReducer} from 'react'
import {FormattedMessage} from 'react-intl'
import {ModalDialog} from '@remix-ui/modal-dialog' // eslint-disable-line
import {Toaster} from '@remix-ui/toaster' // eslint-disable-line
import React, { useState, useRef, useReducer, useEffect } from 'react'
import { FormattedMessage } from 'react-intl'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
const _paq = (window._paq = window._paq || []) // eslint-disable-line
import {CustomTooltip} from '@remix-ui/helper'
import { CustomTooltip } from '@remix-ui/helper'
import { TEMPLATE_NAMES } from '@remix-ui/workspace'
interface HomeTabFileProps {
plugin: any
@ -25,7 +26,7 @@ const loadingReducer = (state = loadingInitialState, action) => {
}
}
function HomeTabFile({plugin}: HomeTabFileProps) {
function HomeTabFile({ plugin }: HomeTabFileProps) {
const [state, setState] = useState<{
searchInput: string
showModalDialog: boolean
@ -37,18 +38,55 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
}
importSource: string
toasterMsg: string
recentWorkspaces: Array<string>
}>({
searchInput: '',
showModalDialog: false,
modalInfo: {title: '', loadItem: '', examples: [], prefix: ''},
modalInfo: { title: '', loadItem: '', examples: [], prefix: '' },
importSource: '',
toasterMsg: ''
toasterMsg: '',
recentWorkspaces: []
})
const [, dispatch] = useReducer(loadingReducer, loadingInitialState)
const inputValue = useRef(null)
useEffect(() => {
plugin.on('filePanel', 'setWorkspace', async () => {
let recents = JSON.parse(localStorage.getItem('recentWorkspaces'))
if (!recents) {
recents = []
} else {
setState((prevState) => {
return { ...prevState, recentWorkspaces: recents.slice(0, recents.length <= 3 ? recents.length : 3) }
})
}
})
const deleteSavedWorkspace = (name) => {
const recents = JSON.parse(localStorage.getItem('recentWorkspaces'))
let newRecents = recents
if (!recents) {
newRecents = []
} else {
newRecents = recents.filter((el) => { return el !== name})
localStorage.setItem('recentWorkspaces', JSON.stringify(newRecents))
}
setState((prevState) => {
return { ...prevState, recentWorkspaces: newRecents.slice(0, newRecents.length <= 3 ? newRecents.length : 3) }
})
}
plugin.on('filePanel', 'workspaceDeleted', async (deletedName) => {
deleteSavedWorkspace(deletedName)
})
return () => {
plugin.off('filePanel', 'setWorkspace')
plugin.off('filePanel', 'workspaceDeleted')
}
}, [plugin])
const processLoading = (type: string) => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'importFrom' + type])
const contentImport = plugin.contentImport
@ -57,12 +95,12 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
if ((type === 'ipfs' || type === 'IPFS') && startsWith !== 'ipfs' && startsWith !== 'IPFS') {
setState((prevState) => {
return {...prevState, importSource: startsWith + state.importSource}
return { ...prevState, importSource: startsWith + state.importSource }
})
}
contentImport.import(
state.modalInfo.prefix + state.importSource,
(loadingMsg) => dispatch({tooltip: loadingMsg}),
(loadingMsg) => dispatch({ tooltip: loadingMsg }),
async (error, content, cleanUrl, type, url) => {
if (error) {
toast(error.message || error)
@ -80,22 +118,47 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
}
)
setState((prevState) => {
return {...prevState, showModalDialog: false, importSource: ''}
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
const toast = (message: string) => {
setState((prevState) => {
return {...prevState, toasterMsg: message}
return { ...prevState, toasterMsg: message }
})
}
const createNewFile = async () => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'createNewFile'])
const startCoding = async () => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'startCoding'])
plugin.verticalIcons.select('filePanel')
await plugin.call('filePanel', 'createNewFile')
}
const wName = 'Playground'
const workspaces = await plugin.call('filePanel', 'getWorkspaces')
let createFile = true
if (!workspaces.find((workspace) => workspace.name === wName)) {
await plugin.call('filePanel', 'createWorkspace', wName, 'playground')
createFile = false
}
await plugin.call('filePanel', 'switchToWorkspace', { name: wName, isLocalHost: false })
await plugin.call('filePanel', 'switchToWorkspace', { name: wName, isLocalHost: false }) // calling once is not working.
const content = `// SPDX-License-Identifier: MIT
pragma solidity >=0.6.12 <0.9.0;
contract HelloWorld {
function print() public pure returns (string memory) {
return "Hello World!";
}
}
`
if (createFile) {
const { newPath } = await plugin.call('fileManager', 'writeFileNoRewrite', '/contracts/helloWorld.sol', content)
await plugin.call('fileManager', 'open', newPath)
} else {
await plugin.call('fileManager', 'open', '/contracts/helloWorld.sol')
}
}
const uploadFile = async (target) => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'uploadFile'])
await plugin.call('filePanel', 'uploadFile', target)
@ -120,8 +183,8 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
title: title,
loadItem: loadItem,
examples: examples,
prefix
}
prefix,
},
}
})
}
@ -129,10 +192,15 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
const hideFullMessage = () => {
//eslint-disable-line
setState((prevState) => {
return {...prevState, showModalDialog: false, importSource: ''}
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
const handleSwichToRecentWorkspace = async (e, workspaceName) => {
e.preventDefault()
await plugin.call('filePanel', 'switchToWorkspace', { name: workspaceName, isLocalhost: false })
}
const examples = state.modalInfo.examples.map((urlEl, key) => (
<div key={key} className="p-1 user-select-auto">
<a>{urlEl}</a>
@ -161,15 +229,15 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
{state.modalInfo.prefix && <span className="text-nowrap align-self-center mr-2">ipfs://</span>}
<input
ref={inputValue}
type="text"
name="prompt_text"
id="inputPrompt_text"
type='text'
name='prompt_text'
id='inputPrompt_text'
className="w-100 mt-1 form-control"
data-id="homeTabModalDialogCustomPromptText"
value={state.importSource}
onInput={(e) => {
setState((prevState) => {
return {...prevState, importSource: inputValue.current.value}
return { ...prevState, importSource: inputValue.current.value }
})
}}
/>
@ -178,40 +246,82 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
</ModalDialog>
<Toaster message={state.toasterMsg} />
<div className="justify-content-start mt-1 p-2 d-flex flex-column" id="hTFileSection">
<label style={{fontSize: '1.2rem'}}>
<label style={{ fontSize: '1.2rem' }}>
<FormattedMessage id="home.files" />
</label>
<div className="dflex">
<button className="btn btn-primary p-2 mr-2 border my-1" data-id="homeTabNewFile" style={{width: 'fit-content'}} onClick={() => createNewFile()}>
<FormattedMessage id="home.newFile" />
</button>
<label className="btn p-2 mr-2 border my-1" style={{width: 'fit-content', cursor: 'pointer'}} htmlFor="openFileInput">
<FormattedMessage id="home.openFile" />
</label>
<input
title="open file"
type="file"
id="openFileInput"
onChange={(event) => {
event.stopPropagation()
plugin.verticalIcons.select('filePanel')
uploadFile(event.target)
}}
multiple
/>
<CustomTooltip
placement={'top'}
tooltipId="overlay-tooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id="home.connectToLocalhost" />}
tooltipTextClasses="border bg-light text-dark p-1 pr-3"
>
<button className="btn p-2 border my-1" style={{width: 'fit-content'}} onClick={() => connectToLocalhost()}>
<FormattedMessage id="home.accessFileSystem" />
</button>
</CustomTooltip>
<div className="d-flex flex-column">
<div className="d-flex flex-row">
<CustomTooltip
placement={'top'}
tooltipId="overlay-tooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='home.startCodingPlayground' />}
tooltipTextClasses="border bg-light text-dark p-1 pr-3"
>
<button className="btn btn-primary text-nowrap p-2 mr-2 border my-1" data-id="homeTabStartCoding" style={{ width: 'fit-content' }} onClick={() => startCoding()}>
<FormattedMessage id="home.startCoding" />
</button>
</CustomTooltip>
<CustomTooltip
placement={'top'}
tooltipId="overlay-tooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='home.openFileTooltip' />}
tooltipTextClasses="border bg-light text-dark p-1 pr-3"
>
<span>
<label className="btn text-nowrap p-2 mr-2 border my-1" style={{ width: 'fit-content', cursor: 'pointer' }} htmlFor="openFileInput">
<FormattedMessage id="home.openFile" />
</label>
<input
title="open file"
type="file"
id="openFileInput"
onChange={(event) => {
event.stopPropagation()
plugin.verticalIcons.select('filePanel')
uploadFile(event.target)
}}
multiple
/>
</span>
</CustomTooltip>
<CustomTooltip
placement={'top'}
tooltipId="overlay-tooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id="home.connectToLocalhost" />}
tooltipTextClasses="border bg-light text-dark p-1 pr-3"
>
<button className="btn text-nowrap p-2 border my-1" style={{width: 'fit-content'}} onClick={() => connectToLocalhost()}>
<FormattedMessage id="home.accessFileSystem" />
</button>
</CustomTooltip>
</div>
{(state.recentWorkspaces[0] || state.recentWorkspaces[1] || state.recentWorkspaces[2]) && (
<div className="d-flex flex-column">
<label style={{ fontSize: '0.8rem' }} className="mt-3">
Recent workspaces
</label>
{state.recentWorkspaces[0] && state.recentWorkspaces[0] !== '' && (
<a className="cursor-pointer mb-1 ml-2" href="#" onClick={(e) => handleSwichToRecentWorkspace(e, state.recentWorkspaces[0])}>
{state.recentWorkspaces[0]}
</a>
)}
{state.recentWorkspaces[1] && state.recentWorkspaces[1] !== '' && (
<a className="cursor-pointer mb-1 ml-2" href="#" onClick={(e) => handleSwichToRecentWorkspace(e, state.recentWorkspaces[1])}>
{state.recentWorkspaces[1]}
</a>
)}
{state.recentWorkspaces[2] && state.recentWorkspaces[2] !== '' && (
<a className="cursor-pointer ml-2" href="#" onClick={(e) => handleSwichToRecentWorkspace(e, state.recentWorkspaces[2])}>
{state.recentWorkspaces[2]}
</a>
)}
</div>
)}
</div>
<label style={{fontSize: '0.8rem'}} className="pt-2">
<label style={{ fontSize: '0.8rem' }} className="pt-3">
<FormattedMessage id="home.loadFrom" />
</label>
<div className="d-flex">
@ -221,7 +331,7 @@ function HomeTabFile({plugin}: HomeTabFileProps) {
onClick={() =>
showFullMessage('GitHub', 'github URL', [
'https://github.com/0xcert/ethereum-erc721/src/contracts/tokens/nf-token-metadata.sol',
'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol'
'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol',
])
}
>

@ -107,18 +107,18 @@ function HomeTabGetStarted({plugin}: HomeTabGetStartedProps) {
<WorkspaceTemplate
gsID="sUTLogo"
workspaceTitle="Gnosis Safe MultiSig"
description={intl.formatMessage({
id: 'home.gnosisSafeMultisigTemplateDesc'
})}
callback={() => createWorkspace('gnosisSafeMultisig')}
description={
intl.formatMessage({ id: 'home.gnosisSafeMultisigTemplateDesc' })
}
callback={() => createWorkspace("gnosisSafeMultisig")}
/>
<WorkspaceTemplate
gsID="sUTLogo"
workspaceTitle="0xProject ERC20"
description={intl.formatMessage({
id: 'home.zeroxErc20TemplateDesc'
})}
callback={() => createWorkspace('zeroxErc20')}
description={
intl.formatMessage({ id: 'home.zeroxErc20TemplateDesc' })
}
callback={() => createWorkspace("zeroxErc20")}
/>
<WorkspaceTemplate
gsID="sourcifyLogo"
@ -132,7 +132,7 @@ function HomeTabGetStarted({plugin}: HomeTabGetStartedProps) {
description={intl.formatMessage({
id: 'home.ozerc721TemplateDesc'
})}
callback={() => createWorkspace('ozerc721')}
callback={() => createWorkspace("ozerc721")}
/>
<WorkspaceTemplate
gsID="sUTLogo"
@ -140,7 +140,7 @@ function HomeTabGetStarted({plugin}: HomeTabGetStartedProps) {
description={intl.formatMessage({
id: 'home.ozerc1155TemplateDesc'
})}
callback={() => createWorkspace('ozerc1155')}
callback={() => createWorkspace("ozerc1155")}
/>
<WorkspaceTemplate
gsID="solhintLogo"
@ -148,7 +148,7 @@ function HomeTabGetStarted({plugin}: HomeTabGetStartedProps) {
description={intl.formatMessage({
id: 'home.remixDefaultTemplateDesc'
})}
callback={() => createWorkspace('remixDefault')}
callback={() => createWorkspace("remixDefault")}
/>
</Carousel>
</ThemeContext.Provider>

@ -265,7 +265,7 @@ function LocalPluginForm({closeModal, visible, pluginManager}: LocalPluginFormPr
checked={location === 'sidePanel'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')}
/>
<label className="form-check-label" htmlFor="sidePanel">
<label className="form-check-label" htmlFor="localPluginRadioButtonsidePanelSidePanel">
<FormattedMessage id="pluginManager.localForm.sidePanel" />
</label>
</div>
@ -280,7 +280,7 @@ function LocalPluginForm({closeModal, visible, pluginManager}: LocalPluginFormPr
checked={location === 'mainPanel'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')}
/>
<label className="form-check-label" htmlFor="mainPanel">
<label className="form-check-label" htmlFor="localPluginRadioButtonsidePanelMainPanel">
<FormattedMessage id="pluginManager.localForm.mainPanel" />
</label>
</div>
@ -295,7 +295,7 @@ function LocalPluginForm({closeModal, visible, pluginManager}: LocalPluginFormPr
checked={location === 'none'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')}
/>
<label className="form-check-label" htmlFor="none">
<label className="form-check-label" htmlFor="localPluginRadioButtonsidePanelNone">
<FormattedMessage id="pluginManager.localForm.none" />
</label>
</div>

@ -40,6 +40,10 @@ export const listenOnPluginEvents = (filePanelPlugin) => {
uploadFile(target, dir, cb)
})
plugin.on('filePanel', 'switchToWorkspace', async (workspace) => {
await switchToWorkspace(workspace.name)
})
plugin.on('fileDecorator', 'fileDecoratorsChanged', async (items: fileDecoration[]) => {
setFileDecorators(items)
})

@ -2,7 +2,30 @@ import React from 'react'
import { bufferToHex } from '@ethereumjs/util'
import { hash } from '@remix-project/remix-lib'
import axios, { AxiosResponse } from 'axios'
import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setCurrentWorkspaceBranches, setCurrentWorkspaceCurrentBranch, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace, setCurrentWorkspaceIsGitRepo, setGitConfig } from './payload'
import {
addInputFieldSuccess,
cloneRepositoryFailed,
cloneRepositoryRequest,
cloneRepositorySuccess,
createWorkspaceError,
createWorkspaceRequest,
createWorkspaceSuccess,
displayNotification,
displayPopUp,
fetchWorkspaceDirectoryError,
fetchWorkspaceDirectoryRequest,
fetchWorkspaceDirectorySuccess,
hideNotification,
setCurrentWorkspace,
setCurrentWorkspaceBranches,
setCurrentWorkspaceCurrentBranch,
setDeleteWorkspace,
setMode,
setReadOnlyMode,
setRenameWorkspace,
setCurrentWorkspaceIsGitRepo,
setGitConfig,
} from './payload'
import { addSlash, checkSlash, checkSpecialChars } from '@remix-ui/helper'
import { FileTree, JSONStandardInput, WorkspaceTemplate } from '../types'
@ -16,14 +39,15 @@ import { AppModal, ModalTypes } from '@remix-ui/app'
import { contractDeployerScripts, etherscanScripts } from '@remix-project/remix-ws-templates'
declare global {
interface Window { remixFileSystemCallback: IndexedDBStorage; }
interface Window {
remixFileSystemCallback: IndexedDBStorage
}
}
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
const queryParams = new QueryParams()
const _paq = window._paq = window._paq || [] //eslint-disable-line
const _paq = (window._paq = window._paq || []) //eslint-disable-line
let plugin, dispatch: React.Dispatch<any>
export const setPlugin = (filePanelPlugin, reducerDispatch) => {
@ -70,19 +94,29 @@ export const addInputField = async (type: 'file' | 'folder', path: string, cb?:
})
})
promise.then((files) => {
dispatch(addInputFieldSuccess(path, files, type))
}).catch((error) => {
console.error(error)
})
promise
.then((files) => {
dispatch(addInputFieldSuccess(path, files, type))
})
.catch((error) => {
console.error(error)
})
return promise
}
const removeSlash = (s: string) => {
return s.replace(/^\/+/, "")
}
export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, opts = null, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void, isGitRepo: boolean = false, createCommit: boolean = true) => {
return s.replace(/^\/+/, '')
}
export const createWorkspace = async (
workspaceName: string,
workspaceTemplateName: WorkspaceTemplate,
opts = null,
isEmpty = false,
cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void,
isGitRepo: boolean = false,
createCommit: boolean = true
) => {
await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName)
dispatch(createWorkspaceRequest())
@ -121,7 +155,7 @@ export const createWorkspace = async (workspaceName: string, workspaceTemplateNa
await plugin.call('dGitProvider', 'commit', {
author: {
name,
email
email,
},
message: `Initial commit: remix template ${workspaceTemplateName}`,
})
@ -138,7 +172,6 @@ export const createWorkspace = async (workspaceName: string, workspaceTemplateNa
}
if (workspaceTemplateName === 'semaphore') {
const isCircomActive = await plugin.call('manager', 'isActive', 'circuit-compiler')
if (!isCircomActive) await plugin.call('manager', 'activatePlugin', 'circuit-compiler')
}
// this call needs to be here after the callback because it calls dGitProvider which also calls this function and that would cause an infinite loop
@ -153,18 +186,17 @@ export const createWorkspace = async (workspaceName: string, workspaceTemplateNa
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 === 'remixDefault') throw new Error('workspace already exists')
if ((await workspaceExists(workspaceName)) && template === 'remixDefault') throw new Error('workspace already exists')
else {
const workspaceProvider = plugin.fileProviders.workspace
await workspaceProvider.createWorkspace(workspaceName)
}
}
export type UrlParametersType = {
gist: string,
code: string,
url: string,
gist: string
code: string
url: string
language: string
}
@ -176,7 +208,8 @@ export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDe
case 'code-template':
// creates a new workspace code-sample and loads code from url params.
try {
let path = ''; let content
let path = ''
let content
if (params.code) {
const hashed = bufferToHex(hash.keccakFromString(params.code))
@ -193,7 +226,7 @@ export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDe
try {
content = JSON.parse(content) as any
if (content.language && content.language === "Solidity" && content.sources) {
if (content.language && content.language === 'Solidity' && content.sources) {
const standardInput: JSONStandardInput = content as JSONStandardInput
for (const [fname, source] of Object.entries(standardInput.sources)) {
await workspaceProvider.set(fname, source.content)
@ -221,7 +254,18 @@ export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDe
const data = response.data as { files: any }
if (!data.files) {
return dispatch(displayNotification('Gist load error', 'No files found', 'OK', null, () => { dispatch(hideNotification()) }, null))
return dispatch(
displayNotification(
'Gist load error',
'No files found',
'OK',
null,
() => {
dispatch(hideNotification())
},
null
)
)
}
const obj = {}
@ -232,11 +276,22 @@ export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDe
})
plugin.fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => {
if (errorLoadingFile) {
dispatch(displayNotification('', errorLoadingFile.message || errorLoadingFile, 'OK', null, () => { }, null))
dispatch(displayNotification('', errorLoadingFile.message || errorLoadingFile, 'OK', null, () => {}, null))
}
})
} catch (e) {
dispatch(displayNotification('Gist load error', e.message, 'OK', null, () => { dispatch(hideNotification()) }, null))
dispatch(
displayNotification(
'Gist load error',
e.message,
'OK',
null,
() => {
dispatch(hideNotification())
},
null
)
)
console.error(e)
}
break
@ -256,7 +311,18 @@ export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDe
}
}
} catch (e) {
dispatch(displayNotification('Workspace load error', e.message, 'OK', null, () => { dispatch(hideNotification()) }, null))
dispatch(
displayNotification(
'Workspace load error',
e.message,
'OK',
null,
() => {
dispatch(hideNotification())
},
null
)
)
console.error(e)
}
break
@ -277,17 +343,18 @@ export const fetchWorkspaceDirectory = async (path: string) => {
const promise: Promise<FileTree> = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree: FileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
dispatch(fetchWorkspaceDirectoryRequest())
promise.then((fileTree) => {
dispatch(fetchWorkspaceDirectorySuccess(path, fileTree))
}).catch((error) => {
dispatch(fetchWorkspaceDirectoryError(error.message))
})
promise
.then((fileTree) => {
dispatch(fetchWorkspaceDirectorySuccess(path, fileTree))
})
.catch((error) => {
dispatch(fetchWorkspaceDirectoryError(error.message))
})
return promise
}
@ -295,6 +362,7 @@ export const renameWorkspace = async (oldName: string, workspaceName: string, cb
await renameWorkspaceFromProvider(oldName, workspaceName)
await dispatch(setRenameWorkspace(oldName, workspaceName))
await plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
await plugin.deleteWorkspace(oldName)
await plugin.workspaceRenamed(oldName, workspaceName)
cb && cb(null, workspaceName)
}
@ -319,7 +387,9 @@ export const deleteWorkspace = async (workspaceName: string, cb?: (err: Error, r
}
export const deleteAllWorkspaces = async () => {
await (await getWorkspaces()).map(async workspace => {
await (
await getWorkspaces()
).map(async (workspace) => {
await deleteWorkspaceFromProvider(workspace.name)
await dispatch(setDeleteWorkspace(workspace.name))
plugin.workspaceDeleted(workspace.name)
@ -356,7 +426,6 @@ export const switchToWorkspace = async (name: string) => {
if (isGitRepo) {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
}
dispatch(setMode('browser'))
@ -370,18 +439,18 @@ const loadFile = (name, file, provider, cb?): void => {
fileReader.onload = async function (event) {
if (checkSpecialChars(file.name)) {
return dispatch(displayNotification('File Upload Failed', 'Special characters are not allowed', 'Close', null, async () => { }))
return dispatch(displayNotification('File Upload Failed', 'Special characters are not allowed', 'Close', null, async () => {}))
}
try {
await provider.set(name, event.target.result)
} catch (error) {
return dispatch(displayNotification('File Upload Failed', 'Failed to create file ' + name, 'Close', null, async () => { }))
return dispatch(displayNotification('File Upload Failed', 'Failed to create file ' + name, 'Close', null, async () => {}))
}
const config = plugin.registry.get('config').api
const editor = plugin.registry.get('editor').api
if ((config.get('currentFile') === name) && (editor.currentContent() !== event.target.result)) {
if (config.get('currentFile') === name && editor.currentContent() !== event.target.result) {
editor.setText(name, event.target.result)
}
}
@ -394,11 +463,11 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
// the files module. Please ask the user here if they want to overwrite
// a file and then just use `files.add`. The file explorer will
// pick that up via the 'fileAdded' event from the files module.
[...target.files].forEach(async (file) => {
;[...target.files].forEach(async (file) => {
const workspaceProvider = plugin.fileProviders.workspace
const name = targetFolder === '/' ? file.name : `${targetFolder}/${file.name}`
if (!await workspaceProvider.exists(name)) {
if (!(await workspaceProvider.exists(name))) {
loadFile(name, file, workspaceProvider, cb)
} else {
const modalContent: AppModal = {
@ -412,7 +481,7 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
loadFile(name, file, workspaceProvider, cb)
},
cancelFn: () => {},
hideFn: () => {}
hideFn: () => {},
}
plugin.call('notification', 'modal', modalContent)
}
@ -420,10 +489,10 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
}
export const uploadFolder = async (target, targetFolder: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
for(const file of [...target.files]) {
for (const file of [...target.files]) {
const workspaceProvider = plugin.fileProviders.workspace
const name = targetFolder === '/' ? file.webkitRelativePath : `${targetFolder}/${file.webkitRelativePath}`
if (!await workspaceProvider.exists(name)) {
if (!(await workspaceProvider.exists(name))) {
loadFile(name, file, workspaceProvider, cb)
} else {
const modalContent: AppModal = {
@ -437,51 +506,53 @@ export const uploadFolder = async (target, targetFolder: string, cb?: (err: Erro
loadFile(name, file, workspaceProvider, cb)
},
cancelFn: () => {},
hideFn: () => {}
hideFn: () => {},
}
plugin.call('notification', 'modal', modalContent)
}
}
}
export const getWorkspaces = async (): Promise<{ name: string, isGitRepo: boolean, branches?: { remote: any; name: string; }[], currentBranch?: string }[]> | undefined => {
export const getWorkspaces = async (): Promise<{ name: string; isGitRepo: boolean; branches?: { remote: any; name: string }[]; currentBranch?: string }[]> | undefined => {
try {
const workspaces: { name: string, isGitRepo: boolean, branches?: { remote: any; name: string; }[], currentBranch?: string }[] = await new Promise((resolve, reject) => {
const workspaces: { name: string; isGitRepo: boolean; branches?: { remote: any; name: string }[]; currentBranch?: string }[] = await new Promise((resolve, reject) => {
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) {
return reject(error)
}
Promise.all(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map(async (folder) => {
const isGitRepo: boolean = await plugin.fileProviders.browser.exists('/' + folder + '/.git')
if (isGitRepo) {
let branches = []
let currentBranch = null
branches = await getGitRepoBranches(folder)
currentBranch = await getGitRepoCurrentBranch(folder)
return {
name: folder.replace(workspacesPath + '/', ''),
isGitRepo,
branches,
currentBranch
}
} else {
return {
name: folder.replace(workspacesPath + '/', ''),
isGitRepo
Promise.all(
Object.keys(items)
.filter((item) => items[item].isDirectory)
.map(async (folder) => {
const isGitRepo: boolean = await plugin.fileProviders.browser.exists('/' + folder + '/.git')
if (isGitRepo) {
let branches = []
let currentBranch = null
branches = await getGitRepoBranches(folder)
currentBranch = await getGitRepoCurrentBranch(folder)
return {
name: folder.replace(workspacesPath + '/', ''),
isGitRepo,
branches,
currentBranch,
}
} else {
return {
name: folder.replace(workspacesPath + '/', ''),
isGitRepo,
}
}
}
})).then(workspacesList => resolve(workspacesList))
})
).then((workspacesList) => resolve(workspacesList))
})
})
await plugin.setWorkspaces(workspaces)
return workspaces
} catch (e) { }
} catch (e) {}
}
export const cloneRepository = async (url: string) => {
@ -496,37 +567,40 @@ export const cloneRepository = async (url: string) => {
const promise = plugin.call('dGitProvider', 'clone', repoConfig, repoName, true)
dispatch(cloneRepositoryRequest())
promise.then(async () => {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
promise
.then(async () => {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
await fetchWorkspaceDirectory(ROOT_PATH)
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const branches = await getGitRepoBranches(workspacesPath + '/' + repoName)
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
await fetchWorkspaceDirectory(ROOT_PATH)
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const branches = await getGitRepoBranches(workspacesPath + '/' + repoName)
dispatch(setCurrentWorkspaceBranches(branches))
const currentBranch = await getGitRepoCurrentBranch(workspacesPath + '/' + repoName)
dispatch(setCurrentWorkspaceBranches(branches))
const currentBranch = await getGitRepoCurrentBranch(workspacesPath + '/' + repoName)
dispatch(setCurrentWorkspaceCurrentBranch(currentBranch))
dispatch(cloneRepositorySuccess())
}).catch(() => {
const cloneModal = {
id: 'cloneGitRepository',
title: 'Clone Git Repository',
message: 'An error occurred: Please check that you have the correct URL for the repo. If the repo is private, you need to add your github credentials (with the valid token permissions) in Settings plugin',
modalType: 'modal',
okLabel: 'OK',
okFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
},
hideFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
dispatch(setCurrentWorkspaceCurrentBranch(currentBranch))
dispatch(cloneRepositorySuccess())
})
.catch(() => {
const cloneModal = {
id: 'cloneGitRepository',
title: 'Clone Git Repository',
message:
'An error occurred: Please check that you have the correct URL for the repo. If the repo is private, you need to add your github credentials (with the valid token permissions) in Settings plugin',
modalType: 'modal',
okLabel: 'OK',
okFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
},
hideFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
},
}
}
plugin.call('notification', 'modal', cloneModal)
})
plugin.call('notification', 'modal', cloneModal)
})
} catch (e) {
dispatch(displayPopUp('An error occured: ' + e))
}
@ -540,7 +614,6 @@ export const checkGit = async () => {
dispatch(setCurrentWorkspaceCurrentBranch(currentBranch))
}
export const getRepositoryTitle = async (url: string) => {
const urlArray = url.split('/')
let name = urlArray.length > 0 ? urlArray[urlArray.length - 1] : ''
@ -561,25 +634,24 @@ export const getRepositoryTitle = async (url: string) => {
}
export const getGitRepoBranches = async (workspacePath: string) => {
const gitConfig: { fs: IndexedDBStorage, dir: string } = {
const gitConfig: { fs: IndexedDBStorage; dir: string } = {
fs: window.remixFileSystemCallback,
dir: addSlash(workspacePath)
dir: addSlash(workspacePath),
}
const branches: { remote: any; name: string; }[] = await plugin.call('dGitProvider', 'branches', { ...gitConfig })
const branches: { remote: any; name: string }[] = await plugin.call('dGitProvider', 'branches', { ...gitConfig })
return branches
}
export const getGitRepoCurrentBranch = async (workspaceName: string) => {
const gitConfig: { fs: IndexedDBStorage, dir: string } = {
const gitConfig: { fs: IndexedDBStorage; dir: string } = {
fs: window.remixFileSystemCallback,
dir: addSlash(workspaceName)
dir: addSlash(workspaceName),
}
const currentBranch: string = await plugin.call('dGitProvider', 'currentbranch', { ...gitConfig })
return currentBranch
}
export const showAllBranches = async () => {
console.log('showAllBranches')
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
plugin.call('menuicons', 'select', 'dgit')
@ -618,28 +690,34 @@ export const switchBranch = async (branch: string) => {
okLabel: 'Force Checkout',
okFn: async () => {
dispatch(cloneRepositoryRequest())
plugin.call('dGitProvider', 'checkout', { ref: branch, force: true }, false).then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
dispatch(cloneRepositorySuccess())
}).catch(() => {
dispatch(cloneRepositoryFailed())
})
plugin
.call('dGitProvider', 'checkout', { ref: branch, force: true }, false)
.then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
dispatch(cloneRepositorySuccess())
})
.catch(() => {
dispatch(cloneRepositoryFailed())
})
},
cancelLabel: 'Cancel',
cancelFn: () => { },
hideFn: () => { }
cancelFn: () => {},
hideFn: () => {},
}
plugin.call('notification', 'modal', cloneModal)
} else {
dispatch(cloneRepositoryRequest())
plugin.call('dGitProvider', 'checkout', { ref: branch, force: true }, false).then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
dispatch(cloneRepositorySuccess())
}).catch(() => {
dispatch(cloneRepositoryFailed())
})
plugin
.call('dGitProvider', 'checkout', { ref: branch, force: true }, false)
.then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
dispatch(cloneRepositorySuccess())
})
.catch(() => {
dispatch(cloneRepositoryFailed())
})
}
}
@ -647,48 +725,49 @@ export const createNewBranch = async (branch: string) => {
const promise = plugin.call('dGitProvider', 'branch', { ref: branch, checkout: true }, false)
dispatch(cloneRepositoryRequest())
promise.then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const workspaceName = plugin.fileProviders.workspace.workspace
const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName)
dispatch(setCurrentWorkspaceBranches(branches))
dispatch(cloneRepositorySuccess())
}).catch(() => {
dispatch(cloneRepositoryFailed())
})
promise
.then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const workspaceName = plugin.fileProviders.workspace.workspace
const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName)
dispatch(setCurrentWorkspaceBranches(branches))
dispatch(cloneRepositorySuccess())
})
.catch(() => {
dispatch(cloneRepositoryFailed())
})
return promise
}
export const createSolidityGithubAction = async () => {
const path = '.github/workflows/run-solidity-unittesting.yml'
await plugin.call('fileManager', 'writeFile', path , solTestYml)
await plugin.call('fileManager', 'writeFile', path, solTestYml)
plugin.call('fileManager', 'open', path)
}
export const createTsSolGithubAction = async () => {
const path = '.github/workflows/run-js-test.yml'
await plugin.call('fileManager', 'writeFile', path , tsSolTestYml)
await plugin.call('fileManager', 'writeFile', path, tsSolTestYml)
plugin.call('fileManager', 'open', path)
}
export const createSlitherGithubAction = async () => {
const path = '.github/workflows/run-slither-action.yml'
await plugin.call('fileManager', 'writeFile', path , slitherYml)
await plugin.call('fileManager', 'writeFile', path, slitherYml)
plugin.call('fileManager', 'open', path)
}
const scriptsRef = {
'deployer': contractDeployerScripts,
'etherscan': etherscanScripts
deployer: contractDeployerScripts,
etherscan: etherscanScripts,
}
export const createHelperScripts = async (script: string) => {
if (!scriptsRef[script]) return
await scriptsRef[script](plugin)
plugin.call('notification', 'toast', 'scripts added in the "scripts" folder')
@ -708,38 +787,44 @@ export const checkoutRemoteBranch = async (branch: string, remote: string) => {
okLabel: 'Force Checkout',
okFn: async () => {
dispatch(cloneRepositoryRequest())
plugin.call('dGitProvider', 'checkout', { ref: branch, remote, force: true }, false).then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const workspaceName = plugin.fileProviders.workspace.workspace
const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName)
dispatch(setCurrentWorkspaceBranches(branches))
dispatch(cloneRepositorySuccess())
}).catch(() => {
dispatch(cloneRepositoryFailed())
})
plugin
.call('dGitProvider', 'checkout', { ref: branch, remote, force: true }, false)
.then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const workspaceName = plugin.fileProviders.workspace.workspace
const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName)
dispatch(setCurrentWorkspaceBranches(branches))
dispatch(cloneRepositorySuccess())
})
.catch(() => {
dispatch(cloneRepositoryFailed())
})
},
cancelLabel: 'Cancel',
cancelFn: () => { },
hideFn: () => { }
cancelFn: () => {},
hideFn: () => {},
}
plugin.call('notification', 'modal', cloneModal)
} else {
dispatch(cloneRepositoryRequest())
plugin.call('dGitProvider', 'checkout', { ref: branch, remote, force: true }, false).then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const workspaceName = plugin.fileProviders.workspace.workspace
const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName)
dispatch(setCurrentWorkspaceBranches(branches))
dispatch(cloneRepositorySuccess())
}).catch(() => {
dispatch(cloneRepositoryFailed())
})
plugin
.call('dGitProvider', 'checkout', { ref: branch, remote, force: true }, false)
.then(async () => {
await fetchWorkspaceDirectory(ROOT_PATH)
dispatch(setCurrentWorkspaceCurrentBranch(branch))
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const workspaceName = plugin.fileProviders.workspace.workspace
const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName)
dispatch(setCurrentWorkspaceBranches(branches))
dispatch(cloneRepositorySuccess())
})
.catch(() => {
dispatch(cloneRepositoryFailed())
})
}
}

@ -903,10 +903,10 @@ export function Workspace() {
</span>
) : null}
<span className="d-flex">
<label className="pl-1 form-check-label" htmlFor="workspacesSelect" style={{wordBreak: 'keep-all'}}>
<FormattedMessage id="filePanel.workspace" />
<label className="pl-2 form-check-label" style={{wordBreak: 'keep-all'}}>
<FormattedMessage id='filePanel.workspace' />
</label>
</span>
</span>
</div>
<div className='mx-2'>
<Dropdown id="workspacesSelect" data-id="workspacesSelect" onToggle={toggleDropdown} show={showDropdown}>

@ -7,17 +7,17 @@ import { ViewPlugin } from '@remixproject/engine-web'
export type action = { name: string, type?: Array<WorkspaceElement>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean, group: number }
export interface JSONStandardInput {
language: "Solidity";
settings?: any,
language: 'Solidity'
settings?: any
sources: {
[globalName: string]: {
keccak256?: string;
content: string;
},
};
keccak256?: string
content: string
}
}
}
export type MenuItems = action[]
export type WorkspaceTemplate = 'gist-template' | 'code-template' | 'remixDefault' | 'blank' | 'ozerc20' | 'zeroxErc20' | 'ozerc721' | 'semaphore'
export type WorkspaceTemplate = 'gist-template' | 'code-template' | 'remixDefault' | 'blank' | 'ozerc20' | 'zeroxErc20' | 'ozerc721' | 'playground' | 'semaphore'
export interface WorkspaceProps {
plugin: FilePanelType
}
@ -38,42 +38,42 @@ export interface Modal {
}
export interface FileType {
path: string,
name: string,
isDirectory: boolean,
type: 'folder' | 'file' | 'gist',
path: string
name: string
isDirectory: boolean
type: 'folder' | 'file' | 'gist'
child?: File[]
}
export interface FilePanelType extends ViewPlugin {
setWorkspace: ({ name, isLocalhost }, setEvent: boolean) => void,
createWorkspace: (name: string, workspaceTemplateName: string) => void,
renameWorkspace: (oldName: string, newName: string) => void
compileContractForUml: (path: string) => void
workspaceRenamed: ({ name }) => void,
workspaceCreated: ({ name }) => void,
workspaceDeleted: ({ name }) => void,
workspace?: any // workspace provider,
browser?: any // browser provider
localhost?: any // localhost provider
fileManager? : any
appManager: RemixAppManager
registry?: any // registry
pluginApi?: any
request: {
createWorkspace: () => void,
setWorkspace: (workspaceName: string) => void,
createNewFile: () => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void,
getCurrentWorkspace: () => void
} // api request,
workspaces: any,
registeredMenuItems: MenuItems // menu items
removedMenuItems: MenuItems
initialWorkspace: string,
resetNewFile: () => void,
getWorkspaces: () => string[]
}
setWorkspace: ({ name, isLocalhost }, setEvent: boolean) => void
createWorkspace: (name: string, workspaceTemplateName: string) => void
renameWorkspace: (oldName: string, newName: string) => void
compileContractForUml: (path: string) => void
workspaceRenamed: ({ name }) => void
workspaceCreated: ({ name }) => void
workspaceDeleted: ({ name }) => void
workspace?: any // workspace provider,
browser?: any // browser provider
localhost?: any // localhost provider
fileManager?: any
appManager: RemixAppManager
registry?: any // registry
pluginApi?: any
request: {
createWorkspace: () => void
setWorkspace: (workspaceName: string) => void
createNewFile: () => void
uploadFile: (target: EventTarget & HTMLInputElement) => void
getCurrentWorkspace: () => void
} // api request,
workspaces: any
registeredMenuItems: MenuItems // menu items
removedMenuItems: MenuItems
initialWorkspace: string
resetNewFile: () => void
getWorkspaces: () => string[]
}
/* eslint-disable-next-line */
export interface FileExplorerProps {
@ -128,41 +128,41 @@ export interface FileExplorerProps {
}
type Placement = import('react-overlays/usePopper').Placement
export interface FileExplorerMenuProps {
title: string,
menuItems: string[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
publishToGist: (path?: string) => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void
uploadFolder: (target: EventTarget & HTMLInputElement) => void
tooltipPlacement?: Placement
title: string
menuItems: string[]
createNewFile: (folder?: string) => void
createNewFolder: (parentFolder?: string) => void
publishToGist: (path?: string) => void
uploadFile: (target: EventTarget & HTMLInputElement) => void
uploadFolder: (target: EventTarget & HTMLInputElement) => void
tooltipPlacement?: Placement
}
export interface FileExplorerContextMenuProps {
actions: action[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => void,
downloadPath: (path: string) => void,
hideContextMenu: () => void,
publishToGist?: (path?: string, type?: string) => void,
pushChangesToGist?: (path?: string, type?: string) => void,
publishFolderToGist?: (path?: string, type?: string) => void,
publishFileToGist?: (path?: string, type?: string) => void,
runScript?: (path: string) => void,
emit?: (cmd: customAction) => void,
pageX: number,
pageY: number,
path: string,
type: string,
focus: {key:string, type:string}[],
onMouseOver?: (...args) => void,
copy?: (path: string, type: string) => void,
paste?: (destination: string, type: string) => void
copyFileName?: (path: string, type: string) => void
copyPath?: (path: string, type: string) => void
generateUml?: (path: string) => Promise<void>
uploadFile?: (target: EventTarget & HTMLInputElement) => void
actions: action[]
createNewFile: (folder?: string) => void
createNewFolder: (parentFolder?: string) => void
deletePath: (path: string | string[]) => void
renamePath: (path: string, type: string) => void
downloadPath: (path: string) => void
hideContextMenu: () => void
publishToGist?: (path?: string, type?: string) => void
pushChangesToGist?: (path?: string, type?: string) => void
publishFolderToGist?: (path?: string, type?: string) => void
publishFileToGist?: (path?: string, type?: string) => void
runScript?: (path: string) => void
emit?: (cmd: customAction) => void
pageX: number
pageY: number
path: string
type: string
focus: { key: string; type: string }[]
onMouseOver?: (...args) => void
copy?: (path: string, type: string) => void
paste?: (destination: string, type: string) => void
copyFileName?: (path: string, type: string) => void
copyPath?: (path: string, type: string) => void
generateUml?: (path: string) => Promise<void>
uploadFile?: (target: EventTarget & HTMLInputElement) => void
}
export interface WorkSpaceState {
@ -190,9 +190,9 @@ export interface WorkSpaceState {
showContextMenu: boolean
reservedKeywords: string[]
copyElement: CopyElementType[]
}
}
export type FileFocusContextType = {
export type FileFocusContextType = {
element: string
x: number
y: number

@ -81,5 +81,6 @@ export const TEMPLATE_NAMES = {
'ozerc1155': 'OpenZeppelin ERC1155',
'zeroxErc20': '0xProject ERC20',
'gnosisSafeMultisig': 'Gnosis Safe',
'playground': 'Playground',
'semaphore': 'Semaphore'
}

@ -5,6 +5,7 @@ export { default as ozerc721 } from './templates/ozerc721'
export { default as ozerc1155 } from './templates/ozerc1155'
export { default as zeroxErc20 } from './templates/zeroxErc20'
export { default as gnosisSafeMultisig } from './templates/gnosisSafeMultisig'
export { default as playground } from './templates/playground'
export { default as semaphore } from './templates/semaphore'
export { contractDeployerScripts } from './script-templates/contract-deployer'

@ -0,0 +1,38 @@
{
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false
}
},
{
"files": "*.yml",
"options": {}
},
{
"files": "*.yaml",
"options": {}
},
{
"files": "*.toml",
"options": {}
},
{
"files": "*.json",
"options": {}
},
{
"files": "*.js",
"options": {}
},
{
"files": "*.ts",
"options": {}
}
]
}

@ -0,0 +1,28 @@
REMIX DEFAULT WORKSPACE
Remix default workspace is present when:
i. Remix loads for the very first time
ii. A new workspace is created with 'Default' template
iii. There are no files existing in the File Explorer
This workspace contains 3 directories:
1. 'contracts': Holds three contracts with increasing levels of complexity.
2. 'scripts': Contains four typescript files to deploy a contract. It is explained below.
3. 'tests': Contains one Solidity test file for 'Ballot' contract & one JS test file for 'Storage' contract.
SCRIPTS
The 'scripts' folder has four typescript files which help to deploy the 'Storage' contract using 'web3.js' and 'ethers.js' libraries.
For the deployment of any other contract, just update the contract's name from 'Storage' to the desired contract and provide constructor arguments accordingly
in the file `deploy_with_ethers.ts` or `deploy_with_web3.ts`
In the 'tests' folder there is a script containing Mocha-Chai unit tests for 'Storage' contract.
To run a script, right click on file name in the file explorer and click 'Run'. Remember, Solidity file must already be compiled.
Output from script will appear in remix terminal.
Please note, require/import is supported in a limited manner for Remix supported modules.
For now, modules supported by Remix are ethers, web3, swarmgw, chai, multihashes, remix and hardhat only for hardhat.ethers object/plugin.
For unsupported modules, an error like this will be thrown: '<module_name> module require is not supported by Remix IDE' will be shown.

@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.12 <0.9.0;
contract HelloWorld {
function print() public pure returns (string memory) {
return "Hello World!";
}
}

@ -0,0 +1,16 @@
export default async () => {
return {
// @ts-ignore
'contracts/helloWorld.sol': (await import('raw-loader!./contracts/helloWorld.sol')).default,
// @ts-ignore
'scripts/deploy_with_ethers.ts': (await import('!!raw-loader!./scripts/deploy_with_ethers.ts')).default,
// @ts-ignore
'scripts/deploy_with_web3.ts': (await import('!!raw-loader!./scripts/deploy_with_web3.ts')).default,
// @ts-ignore
'scripts/ethers-lib.ts': (await import('!!raw-loader!./scripts/ethers-lib.ts')).default,
// @ts-ignore
'scripts/web3-lib.ts': (await import('!!raw-loader!./scripts/web3-lib.ts')).default,
// @ts-ignore
'.prettierrc.json': (await import('raw-loader!./.prettierrc')).default,
}
}

@ -0,0 +1,14 @@
// This script can be used to deploy the "Storage" contract using ethers.js library.
// Please make sure to compile "./contracts/1_Storage.sol" file before running this script.
// And use Right click -> "Run" from context menu of the file to run the script. Shortcut: Ctrl+Shift+S
import { deploy } from './ethers-lib'
(async () => {
try {
const result = await deploy('HelloWorld', [])
console.log(`address: ${result.address}`)
} catch (e) {
console.log(e.message)
}
})()

@ -0,0 +1,14 @@
// This script can be used to deploy the "Storage" contract using Web3 library.
// Please make sure to compile "./contracts/1_Storage.sol" file before running this script.
// And use Right click -> "Run" from context menu of the file to run the script. Shortcut: Ctrl+Shift+S
import { deploy } from './web3-lib'
(async () => {
try {
const result = await deploy('HelloWorld', [])
console.log(`address: ${result.address}`)
} catch (e) {
console.log(e.message)
}
})()

@ -0,0 +1,29 @@
import { ethers } from 'ethers'
/**
* Deploy the given contract
* @param {string} contractName name of the contract to deploy
* @param {Array<any>} args list of constructor' parameters
* @param {Number} accountIndex account index from the exposed account
* @return {Contract} deployed contract
*/
export const deploy = async (contractName: string, args: Array<any>, accountIndex?: number): Promise<ethers.Contract> => {
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` // Change this for different path
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(accountIndex)
const factory = new ethers.ContractFactory(metadata.abi, metadata.data.bytecode.object, signer)
const contract = await factory.deploy(...args)
// The contract is NOT deployed yet; we must wait until it is mined
await contract.deployed()
return contract
}

@ -0,0 +1,36 @@
import Web3 from 'web3'
import { Contract, ContractSendMethod, Options } from 'web3-eth-contract'
/**
* Deploy the given contract
* @param {string} contractName name of the contract to deploy
* @param {Array<any>} args list of constructor' parameters
* @param {string} from account used to send the transaction
* @param {number} gas gas limit
* @return {Options} deployed contract
*/
export const deploy = async (contractName: string, args: Array<any>, from?: string, gas?: number): Promise<Options> => {
const web3 = new Web3(web3Provider)
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()
const contract: Contract = new web3.eth.Contract(metadata.abi)
const contractSend: ContractSendMethod = contract.deploy({
data: metadata.data.bytecode.object,
arguments: args
})
const newContractInstance = await contractSend.send({
from: from || accounts[0],
gas: gas || 1500000
})
return newContractInstance.options
}
Loading…
Cancel
Save