diff --git a/apps/remix-ide-e2e/src/commands/addFile.ts b/apps/remix-ide-e2e/src/commands/addFile.ts index 80838ff9d7..aa1f8d7f6e 100644 --- a/apps/remix-ide-e2e/src/commands/addFile.ts +++ b/apps/remix-ide-e2e/src/commands/addFile.ts @@ -18,9 +18,9 @@ function addFile (browser: NightwatchBrowser, name: string, content: NightwatchC .clickLaunchIcon('filePanel') .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('.newFile') - .waitForElementContainsText('*[data-id="treeViewLitreeViewItem/blank"]', '', 60000) - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', name) - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) + .waitForElementContainsText('*[data-id$="/blank"]', '', 60000) + .sendKeys('*[data-id$="/blank"] .remixui_items', name) + .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) .pause(2000) .waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000) .setEditorValue(content.content) diff --git a/apps/remix-ide-e2e/src/helpers/init.ts b/apps/remix-ide-e2e/src/helpers/init.ts index 484645b391..8782c7a44b 100644 --- a/apps/remix-ide-e2e/src/helpers/init.ts +++ b/apps/remix-ide-e2e/src/helpers/init.ts @@ -2,11 +2,18 @@ import { NightwatchBrowser } from 'nightwatch' require('dotenv').config() -export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true): void { +export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true, closeWorkspaceAlert = true): void { browser .url(url || 'http://127.0.0.1:8080') .pause(5000) .switchBrowserTab(0) + .perform((done) => { + if (closeWorkspaceAlert) { + browser.waitForElementVisible('*[data-id="modalDialogModalBody"]', 60000) + .modalFooterOKClick() + } + done() + }) .fullscreenWindow(() => { if (preloadPlugins) { initModules(browser, () => { diff --git a/apps/remix-ide-e2e/src/tests/debugger.spec.ts b/apps/remix-ide-e2e/src/tests/debugger.spec.ts index ac2f1489fc..b43b94559b 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.spec.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.spec.ts @@ -323,7 +323,7 @@ const localVariable_step266_ABIEncoder = { // eslint-disable-line value: '0x0000000000000000000000000000000000000000000000000000000000000002' }, userData: { - error: '', + value: '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000015b38da6a701c568545dcfcb03fcb875f56beddc4', type: 'bytes' } } @@ -360,7 +360,7 @@ const localVariable_step717_ABIEncoder = { // eslint-disable-line value: '0x5b38da6a701c568545dcfcb03fcb875f56beddc45b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001' }, userData: { - error: '', + value: '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000015b38da6a701c568545dcfcb03fcb875f56beddc4', type: 'bytes' } } diff --git a/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts b/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts index 0400dc6394..4eb8d156b6 100644 --- a/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts +++ b/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts @@ -22,9 +22,9 @@ module.exports = { .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('*[data-id="fileExplorerNewFilecreateNewFile"]') .pause(1000) - .waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', '5_New_contract.sol') - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) + .waitForElementVisible('*[data-id$="/blank"]') + .sendKeys('*[data-id$="/blank"] .remixui_items', '5_New_contract.sol') + .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) .waitForElementVisible('*[data-id="treeViewLitreeViewItem5_New_contract.sol"]', 7000) }, @@ -49,9 +49,9 @@ module.exports = { .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('[data-id="fileExplorerNewFilecreateNewFolder"]') .pause(1000) - .waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', 'Browser_Tests') - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) + .waitForElementVisible('*[data-id$="/blank"]') + .sendKeys('*[data-id$="/blank"] .remixui_items', 'Browser_Tests') + .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]') }, diff --git a/apps/remix-ide-e2e/src/tests/gist.spec.ts b/apps/remix-ide-e2e/src/tests/gist.spec.ts index dbc2f3a096..ad98cb6f19 100644 --- a/apps/remix-ide-e2e/src/tests/gist.spec.ts +++ b/apps/remix-ide-e2e/src/tests/gist.spec.ts @@ -29,9 +29,9 @@ module.exports = { .waitForElementVisible('*[data-id="fileExplorerNewFilecreateNewFolder"]') .click('[data-id="fileExplorerNewFilecreateNewFolder"]') .pause(1000) - .waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', 'Browser_Tests') - .sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) + .waitForElementVisible('*[data-id$="/blank"]') + .sendKeys('*[data-id$="/blank"] .remixui_items', 'Browser_Tests') + .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]') .addFile('File.sol', { content: '' }) .click('*[data-id="fileExplorerNewFilepublishToGist"]') diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts index 3601539f3d..9223ae1728 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts @@ -179,9 +179,8 @@ function runTests (browser: NightwatchBrowser) { .click('*[data-id="treeViewLitreeViewItemcontracts"]') .openFile('contracts/3_Ballot.sol') .clickLaunchIcon('solidityUnitTesting') - .pause(500) - .setValue('*[data-id="uiPathInput"]', 'tests') .pause(2000) + .verify.attributeEquals('*[data-id="uiPathInput"]', 'value', 'tests') .scrollAndClick('#runTestsTabRunAction') .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) diff --git a/apps/remix-ide-e2e/src/tests/staticAnalysis.spec.ts b/apps/remix-ide-e2e/src/tests/staticAnalysis.spec.ts index a68a4a24ce..504148b266 100644 --- a/apps/remix-ide-e2e/src/tests/staticAnalysis.spec.ts +++ b/apps/remix-ide-e2e/src/tests/staticAnalysis.spec.ts @@ -40,7 +40,7 @@ function runTests (browser: NightwatchBrowser) { .pause(10000) .testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['TooMuchGas', 'test1', 'test2']) .clickLaunchIcon('solidityStaticAnalysis') - .click('#staticanalysisView button') + .click('#staticanalysisButton button') .waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () { listSelectorContains(['Use of tx.origin', 'Fallback function of contract TooMuchGas requires too much gas', diff --git a/apps/remix-ide-e2e/src/tests/url.spec.ts b/apps/remix-ide-e2e/src/tests/url.spec.ts index ec273cab4c..40a52bd073 100644 --- a/apps/remix-ide-e2e/src/tests/url.spec.ts +++ b/apps/remix-ide-e2e/src/tests/url.spec.ts @@ -10,7 +10,7 @@ const sources = [ module.exports = { before: function (browser: NightwatchBrowser, done: VoidFunction) { - init(browser, done, 'http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0') + init(browser, done, 'http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0', true, false) }, '@sources': function () { diff --git a/apps/remix-ide/src/app/compiler/compiler-imports.js b/apps/remix-ide/src/app/compiler/compiler-imports.js index 89fa1aa988..0e72bb425d 100644 --- a/apps/remix-ide/src/app/compiler/compiler-imports.js +++ b/apps/remix-ide/src/app/compiler/compiler-imports.js @@ -121,9 +121,7 @@ module.exports = class CompilerImports extends Plugin { if (provider.type === 'localhost' && !provider.isConnected()) { return reject(new Error(`file provider ${provider.type} not available while trying to resolve ${url}`)) } - provider.exists(url, (error, exist) => { - if (error) return reject(error) - + provider.exists(url).then(exist => { /* if the path is absolute and the file does not exist, we can stop here Doesn't make sense to try to resolve "localhost/node_modules/localhost/node_modules/" and we'll end in an infinite loop. @@ -162,6 +160,8 @@ module.exports = class CompilerImports extends Plugin { if (error) return reject(error) resolve(content) }) + }).catch(error => { + return reject(error) }) } }) diff --git a/apps/remix-ide/src/app/editor/contextView.js b/apps/remix-ide/src/app/editor/contextView.js index dbf1bbf933..da01321762 100644 --- a/apps/remix-ide/src/app/editor/contextView.js +++ b/apps/remix-ide/src/app/editor/contextView.js @@ -109,10 +109,11 @@ class ContextView { if (filename !== this._deps.config.get('currentFile')) { const provider = this._deps.fileManager.fileProviderOf(filename) if (provider) { - provider.exists(filename, (error, exist) => { - if (error) return console.log(error) + provider.exists(filename).then(exist => { this._deps.fileManager.open(filename) jumpToLine(lineColumn) + }).catch(error => { + if (error) return console.log(error) }) } } else { diff --git a/apps/remix-ide/src/app/files/fileManager.js b/apps/remix-ide/src/app/files/fileManager.js index cc73ba0942..fd74b1722d 100644 --- a/apps/remix-ide/src/app/files/fileManager.js +++ b/apps/remix-ide/src/app/files/fileManager.js @@ -121,10 +121,7 @@ class FileManager extends Plugin { try { path = this.limitPluginScope(path) const provider = this.fileProviderOf(path) - const result = provider.exists(path, (err, result) => { - if (err) return false - return result - }) + const result = provider.exists(path) return result } catch (e) { diff --git a/apps/remix-ide/src/app/files/fileProvider.js b/apps/remix-ide/src/app/files/fileProvider.js index 544a1b0bab..521dcec453 100644 --- a/apps/remix-ide/src/app/files/fileProvider.js +++ b/apps/remix-ide/src/app/files/fileProvider.js @@ -63,11 +63,11 @@ class FileProvider { }) } - exists (path, cb) { + async exists (path) { // todo check the type (directory/file) as well #2386 // currently it is not possible to have a file and folder with same path const ret = this._exists(path) - if (cb) cb(null, ret) + return ret } diff --git a/apps/remix-ide/src/app/files/remixDProvider.js b/apps/remix-ide/src/app/files/remixDProvider.js index b20a097ccd..60337c11e4 100644 --- a/apps/remix-ide/src/app/files/remixDProvider.js +++ b/apps/remix-ide/src/app/files/remixDProvider.js @@ -74,16 +74,15 @@ module.exports = class RemixDProvider extends FileProvider { }) } - exists (path, cb) { - if (!this._isReady) return cb && cb('provider not ready') + exists (path) { + if (!this._isReady) throw new Error('provider not ready') const unprefixedpath = this.removePrefix(path) return this._appManager.call('remixd', 'exists', { path: unprefixedpath }) .then((result) => { - if (cb) return cb(null, result) return result - }).catch((error) => { - if (cb) return cb(error) + }) + .catch((error) => { throw new Error(error) }) } diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/files/remixd-handle.js index b5f9ec9804..3ec635040c 100644 --- a/apps/remix-ide/src/app/files/remixd-handle.js +++ b/apps/remix-ide/src/app/files/remixd-handle.js @@ -135,7 +135,7 @@ function remixdDialog () {
If you have looked at the Remixd docs and just need remixd command,
here it is:
remixd -s absolute-path-to-the-shared-folder --remix-ide your-remix-ide-URL-instance
-
Connection will start a session between ${window.location.href} and your local file system ws://127.0.0.1:65520 +
Connection will start a session between ${window.location.origin} and your local file system ws://127.0.0.1:65520 so please make sure your system is secured enough (port 65520 neither opened nor forwarded).
diff --git a/apps/remix-ide/src/app/files/workspaceFileProvider.js b/apps/remix-ide/src/app/files/workspaceFileProvider.js index 742d85a775..156723b622 100644 --- a/apps/remix-ide/src/app/files/workspaceFileProvider.js +++ b/apps/remix-ide/src/app/files/workspaceFileProvider.js @@ -33,7 +33,7 @@ class WorkspaceFileProvider extends FileProvider { if (!this.workspace) this.createWorkspace() 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 + if (path.startsWith(this.workspace)) return path.replace(this.workspace, this.workspacesPath + '/' + this.workspace) path = super.removePrefix(path) let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path) diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 893ec084f9..991108285c 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -188,8 +188,11 @@ module.exports = class Filepanel extends ViewPlugin { const browserProvider = this._deps.fileProviders.browser const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath - if (!browserProvider.exists(workspaceRootPath)) browserProvider.createDir(workspaceRootPath) - if (!browserProvider.exists(workspacePath)) browserProvider.createDir(workspacePath) + const workspaceRootPathExists = await browserProvider.exists(workspaceRootPath) + const workspacePathExists = await browserProvider.exists(workspacePath) + + if (!workspaceRootPathExists) browserProvider.createDir(workspaceRootPath) + if (!workspacePathExists) browserProvider.createDir(workspacePath) } async workspaceExists (name) { @@ -209,11 +212,13 @@ module.exports = class Filepanel extends ViewPlugin { workspaceProvider.setWorkspace(workspaceName) await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace for (const file in examples) { - try { - await workspaceProvider.set(examples[file].name, examples[file].content) - } catch (error) { - console.error(error) - } + setTimeout(async () => { // space creation of files to give react ui time to update. + try { + await workspaceProvider.set(examples[file].name, examples[file].content) + } catch (error) { + console.error(error) + } + }, 10) } } } diff --git a/apps/remix-ide/src/app/tabs/analysis-tab.js b/apps/remix-ide/src/app/tabs/analysis-tab.js index 4406ba875e..b3bba2705c 100644 --- a/apps/remix-ide/src/app/tabs/analysis-tab.js +++ b/apps/remix-ide/src/app/tabs/analysis-tab.js @@ -1,9 +1,11 @@ +import React from 'react' // eslint-disable-line import { ViewPlugin } from '@remixproject/engine-web' +import ReactDOM from 'react-dom' import { EventEmitter } from 'events' +import {RemixUiStaticAnalyser} from '@remix-ui/static-analyser' // eslint-disable-line import * as packageJson from '../../../../../package.json' +var Renderer = require('../ui/renderer') -var yo = require('yo-yo') -var StaticAnalysis = require('./staticanalysis/staticAnalysisView') var EventManager = require('../../lib/events') const profile = { @@ -25,23 +27,49 @@ class AnalysisTab extends ViewPlugin { this.event = new EventManager() this.events = new EventEmitter() this.registry = registry + this.element = document.createElement('div') + this.element.setAttribute('id', 'staticAnalyserView') + this._components = { + renderer: new Renderer(this) + } + this._components.registry = this.registry + this._deps = { + offsetToLineColumnConverter: this.registry.get( + 'offsettolinecolumnconverter').api + } + } + + onActivation () { + this.renderComponent() } render () { - this.staticanalysis = new StaticAnalysis(this.registry, this) - this.staticanalysis.event.register('staticAnaysisWarning', (count) => { - if (count > 0) { - this.emit('statusChanged', { key: count, title: `${count} warning${count === 1 ? '' : 's'}`, type: 'warning' }) - } else if (count === 0) { - this.emit('statusChanged', { key: 'succeed', title: 'no warning', type: 'success' }) - } else { - // count ==-1 no compilation result - this.emit('statusChanged', { key: 'none' }) - } - }) - this.registry.put({ api: this.staticanalysis, name: 'staticanalysis' }) + return this.element + } - return yo`
${this.staticanalysis.render()}
` + renderComponent () { + ReactDOM.render( + , + this.element, + () => { + this.event.register('staticAnaysisWarning', (count) => { + if (count > 0) { + this.emit('statusChanged', { key: count, title: `${count} warning${count === 1 ? '' : 's'}`, type: 'warning' }) + } else if (count === 0) { + this.emit('statusChanged', { key: 'succeed', title: 'no warning', type: 'success' }) + } else { + // count ==-1 no compilation result + this.emit('statusChanged', { key: 'none' }) + } + }) + } + ) } } diff --git a/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js b/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js index f64cdead77..88880bff22 100644 --- a/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js +++ b/apps/remix-ide/src/app/tabs/compileTab/compilerContainer.js @@ -517,7 +517,7 @@ class CompilerContainer { // fetching both normal and wasm builds and creating a [version, baseUrl] map async fetchAllVersion (callback) { let selectedVersion, allVersionsWasm, isURL - let allVersions = [{ path: 'builtin', longVersion: 'latest local version - 0.7.4' }] + let allVersions = [{ path: 'builtin', longVersion: 'Stable local version - 0.7.4' }] // fetch normal builds const binRes = await promisedMiniXhr(`${baseURLBin}/list.json`) // fetch wasm builds diff --git a/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js b/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js index fb8f022399..2a8336125d 100644 --- a/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js +++ b/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js @@ -9,6 +9,7 @@ const confirmDialog = require('../../ui/confirmDialog') const modalDialog = require('../../ui/modaldialog') const MultiParamManager = require('../../ui/multiParamManager') const helper = require('../../../lib/helper') +const _paq = window._paq = window._paq || [] class ContractDropdownUI { constructor (blockchain, dropdownLogic, logCallback, runView) { @@ -300,13 +301,15 @@ class ContractDropdownUI { if (error) { return this.logCallback(error) } - self.event.trigger('newContractInstanceAdded', [contractObject, address, contractObject.name]) const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file) self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data) if (self.ipfsCheckedState) { + _paq.push(['trackEvent', 'udapp', `DeployAndPublish_${this.networkName}`]) publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract) + } else { + _paq.push(['trackEvent', 'udapp', `DeployOnly_${this.networkName}`]) } } @@ -340,6 +343,7 @@ class ContractDropdownUI { } deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) { + _paq.push(['trackEvent', 'udapp', 'DeployContractTo', this.networkName]) const { statusCb } = callbacks if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) { return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) diff --git a/apps/remix-ide/src/app/tabs/runTab/model/dropdownlogic.js b/apps/remix-ide/src/app/tabs/runTab/model/dropdownlogic.js index a2d01d02f4..15764a6dbe 100644 --- a/apps/remix-ide/src/app/tabs/runTab/model/dropdownlogic.js +++ b/apps/remix-ide/src/app/tabs/runTab/model/dropdownlogic.js @@ -1,7 +1,8 @@ -var remixLib = require('@remix-project/remix-lib') -var txHelper = remixLib.execution.txHelper -var CompilerAbstract = require('../../../compiler/compiler-abstract') -var EventManager = remixLib.EventManager +const remixLib = require('@remix-project/remix-lib') +const txHelper = remixLib.execution.txHelper +const CompilerAbstract = require('../../../compiler/compiler-abstract') +const EventManager = remixLib.EventManager +const _paq = window._paq = window._paq || [] class DropdownLogic { constructor (compilersArtefacts, config, editor, runView) { @@ -50,9 +51,11 @@ class DropdownLogic { } catch (e) { return cb('Failed to parse the current file as JSON ABI.') } + _paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithABI']) cb(null, 'abi', abi) }) } else { + _paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithInstance']) cb(null, 'instance') } } diff --git a/apps/remix-ide/src/app/tabs/staticanalysis/staticAnalysisView.js b/apps/remix-ide/src/app/tabs/staticanalysis/staticAnalysisView.js deleted file mode 100644 index 668adab89e..0000000000 --- a/apps/remix-ide/src/app/tabs/staticanalysis/staticAnalysisView.js +++ /dev/null @@ -1,302 +0,0 @@ -'use strict' -var StaticAnalysisRunner = require('@remix-project/remix-analyzer').CodeAnalysis -var yo = require('yo-yo') -var $ = require('jquery') -var remixLib = require('@remix-project/remix-lib') -var utils = remixLib.util -var css = require('./styles/staticAnalysisView-styles') -var Renderer = require('../../ui/renderer') -const SourceHighlighter = require('../../editor/sourceHighlighter') - -var EventManager = require('../../../lib/events') - -function staticAnalysisView (localRegistry, analysisModule) { - var self = this - this.event = new EventManager() - this.view = null - this.runner = new StaticAnalysisRunner() - this.modulesView = this.renderModules() - this.lastCompilationResult = null - this.lastCompilationSource = null - this.currentFile = 'No file compiled' - this.sourceHighlighter = new SourceHighlighter() - this.analysisModule = analysisModule - self._components = { - renderer: new Renderer(analysisModule) - } - self._components.registry = localRegistry - // dependencies - self._deps = { - offsetToLineColumnConverter: self._components.registry.get('offsettolinecolumnconverter').api - } - - analysisModule.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => { - self.lastCompilationResult = null - self.lastCompilationSource = null - if (languageVersion.indexOf('soljson') !== 0) return - self.lastCompilationResult = data - self.lastCompilationSource = source - self.currentFile = file - self.correctRunBtnDisabled() - if (self.view && self.view.querySelector('#autorunstaticanalysis').checked) { - self.run() - } - }) -} - -staticAnalysisView.prototype.render = function () { - this.runBtn = yo`` - const view = yo` -
-
-
-
- - -
-
- - -
- ${this.runBtn} -
-
-
- ${this.modulesView} -
-
- last results for: - ${this.currentFile} -
-
-
- ` - - if (!this.view) { - this.view = view - } - this.correctRunBtnDisabled() - return view -} - -staticAnalysisView.prototype.selectedModules = function () { - if (!this.view) { - return [] - } - const selected = this.view.querySelectorAll('[name="staticanalysismodule"]:checked') - var toRun = [] - for (var i = 0; i < selected.length; i++) { - toRun.push(selected[i].attributes.index.value) - } - return toRun -} - -staticAnalysisView.prototype.run = function () { - if (!this.view) { - return - } - const highlightLocation = async (location, fileName) => { - await this.analysisModule.call('editor', 'discardHighlight') - await this.analysisModule.call('editor', 'highlight', location, fileName) - } - const selected = this.selectedModules() - const warningContainer = $('#staticanalysisresult') - warningContainer.empty() - this.view.querySelector('#staticAnalysisCurrentFile').innerText = this.currentFile - var self = this - if (this.lastCompilationResult && selected.length) { - this.runBtn.removeAttribute('disabled') - let warningCount = 0 - this.runner.run(this.lastCompilationResult, selected, (results) => { - const groupedModules = utils.groupBy(preProcessModules(this.runner.modules()), 'categoryId') - results.map((result, j) => { - let moduleName - Object.keys(groupedModules).map((key) => { - groupedModules[key].forEach((el) => { - if (el.name === result.name) { - moduleName = groupedModules[key][0].categoryDisplayName - } - }) - }) - const alreadyExistedEl = this.view.querySelector(`[id="staticAnalysisModule${moduleName}"]`) - if (!alreadyExistedEl) { - warningContainer.append(` -
- ${moduleName} -
- `) - } - - result.report.map((item, i) => { - let location = '' - let locationString = 'not available' - let column = 0 - let row = 0 - let fileName = this.currentFile - if (item.location) { - var split = item.location.split(':') - var file = split[2] - location = { - start: parseInt(split[0]), - length: parseInt(split[1]) - } - location = self._deps.offsetToLineColumnConverter.offsetToLineColumn( - location, - parseInt(file), - self.lastCompilationSource.sources, - self.lastCompilationResult.sources - ) - row = location.start.line - column = location.start.column - locationString = (row + 1) + ':' + column + ':' - fileName = Object.keys(self.lastCompilationResult.contracts)[file] - } - warningCount++ - const msg = yo` - - ${result.name} - ${item.warning} - ${item.more ? yo`more` : yo``} - Pos: ${locationString} - ` - self._components.renderer.error( - msg, - this.view.querySelector(`[id="staticAnalysisModule${moduleName}"]`), - { - click: () => highlightLocation(location, fileName), - type: 'warning', - useSpan: true, - errFile: fileName, - errLine: row, - errCol: column - } - ) - }) - }) - // hide empty staticAnalysisModules sections - this.view.querySelectorAll('[name="staticAnalysisModules"]').forEach((section) => { - if (!section.getElementsByClassName('alert-warning').length) section.hidden = true - }) - self.event.trigger('staticAnaysisWarning', [warningCount]) - }) - } else { - this.runBtn.setAttribute('disabled', 'disabled') - if (selected.length) { - warningContainer.html('No compiled AST available') - } - self.event.trigger('staticAnaysisWarning', [-1]) - } -} -staticAnalysisView.prototype.checkModule = function (event) { - const selected = this.view.querySelectorAll('[name="staticanalysismodule"]:checked') - const checkAll = this.view.querySelector('[id="checkAllEntries"]') - this.correctRunBtnDisabled() - if (event.target.checked) { - checkAll.checked = true - } else if (!selected.length) { - checkAll.checked = false - } -} -staticAnalysisView.prototype.correctRunBtnDisabled = function () { - if (!this.view) { - return - } - const selected = this.view.querySelectorAll('[name="staticanalysismodule"]:checked') - if (this.lastCompilationResult && selected.length !== 0) { - this.runBtn.removeAttribute('disabled') - } else { - this.runBtn.setAttribute('disabled', 'disabled') - } -} -staticAnalysisView.prototype.checkAll = function (event) { - if (!this.view) { - return - } - // checks/unchecks all - const checkBoxes = this.view.querySelectorAll('[name="staticanalysismodule"]') - checkBoxes.forEach((checkbox) => { checkbox.checked = event.target.checked }) - this.correctRunBtnDisabled() -} - -staticAnalysisView.prototype.handleCollapse = function (e) { - const downs = e.toElement.parentElement.getElementsByClassName('fas fa-angle-double-right') - const iEls = document.getElementsByTagName('i') - for (var i = 0; i < iEls.length; i++) { iEls[i].hidden = false } - downs[0].hidden = true -} - -staticAnalysisView.prototype.renderModules = function () { - const groupedModules = utils.groupBy(preProcessModules(this.runner.modules()), 'categoryId') - const moduleEntries = Object.keys(groupedModules).map((categoryId, i) => { - const category = groupedModules[categoryId] - const entriesDom = category.map((item, i) => { - return yo` -
- - -
- ` - }) - return yo` -
- this.handleCollapse(e)}"/> - -
- ${entriesDom} -
-
- ` - }) - // collaps first module - moduleEntries[0].getElementsByTagName('input')[0].checked = true - moduleEntries[0].getElementsByTagName('i')[0].hidden = true - return yo` -
- ${moduleEntries} -
` -} - -module.exports = staticAnalysisView - -/** - * @dev Process & categorize static analysis modules to show them on UI - * @param arr list of static analysis modules received from remix-analyzer module - */ -function preProcessModules (arr) { - return arr.map((Item, i) => { - const itemObj = new Item() - itemObj._index = i - itemObj.categoryDisplayName = itemObj.category.displayName - itemObj.categoryId = itemObj.category.id - return itemObj - }) -} diff --git a/apps/remix-ide/src/app/tabs/staticanalysis/styles/staticAnalysisView-styles.js b/apps/remix-ide/src/app/tabs/staticanalysis/styles/staticAnalysisView-styles.js deleted file mode 100644 index bd277a03aa..0000000000 --- a/apps/remix-ide/src/app/tabs/staticanalysis/styles/staticAnalysisView-styles.js +++ /dev/null @@ -1,36 +0,0 @@ -var csjs = require('csjs-inject') - -var css = csjs` - .analysis { - display: flex; - flex-direction: column; - } - .result { - margin-top: 1%; - max-height: 300px; - word-break: break-word; - } - .buttons { - margin: 1rem 0; - } - .label { - display: flex; - align-items: center; - } - .label { - display: flex; - align-items: center; - user-select: none; - } - .block input[type='radio']:checked ~ .entries{ - height: auto; - transition: .5s ease-in; - } - .entries{ - height: 0; - overflow: hidden; - transition: .5s ease-out; - } -` - -module.exports = css diff --git a/apps/remix-ide/src/app/tabs/testTab/testTab.js b/apps/remix-ide/src/app/tabs/testTab/testTab.js index 1cfdff91be..1f915d9ee4 100644 --- a/apps/remix-ide/src/app/tabs/testTab/testTab.js +++ b/apps/remix-ide/src/app/tabs/testTab/testTab.js @@ -18,7 +18,9 @@ class TestTabLogic { // Checking to ignore the value which contains only whitespaces if (!path || !(/\S/.test(path))) return const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) - fileProvider.exists(path, (e, res) => { if (!res) fileProvider.createDir(path) }) + fileProvider.exists(path).then(res => { + if (!res) fileProvider.createDir(path) + }) } pathExists (path) { diff --git a/apps/remix-ide/src/app/udapp/run-tab.js b/apps/remix-ide/src/app/udapp/run-tab.js index b3ba8013be..cc0d2f6ab6 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.js +++ b/apps/remix-ide/src/app/udapp/run-tab.js @@ -15,6 +15,7 @@ const RecorderUI = require('../tabs/runTab/recorder.js') const DropdownLogic = require('../tabs/runTab/model/dropdownlogic.js') const ContractDropdownUI = require('../tabs/runTab/contractDropdown.js') const toaster = require('../ui/tooltip') +const _paq = window._paq = window._paq || [] const UniversalDAppUI = require('../ui/universal-dapp-ui') @@ -91,6 +92,7 @@ export class RunTab extends ViewPlugin { } sendTransaction (tx) { + _paq.push(['trackEvent', 'udapp', 'sendTx']) return this.blockchain.sendTransaction(tx) } diff --git a/apps/remix-ide/src/app/ui/renderer.js b/apps/remix-ide/src/app/ui/renderer.js index 35cf8395ec..97107c19d2 100644 --- a/apps/remix-ide/src/app/ui/renderer.js +++ b/apps/remix-ide/src/app/ui/renderer.js @@ -39,10 +39,11 @@ Renderer.prototype._errorClick = function (errFile, errLine, errCol) { // TODO: refactor with this._components.contextView.jumpTo var provider = self._deps.fileManager.fileProviderOf(errFile) if (provider) { - provider.exists(errFile, (error, exist) => { - if (error) return console.log(error) + provider.exists(errFile).then(exist => { self._deps.fileManager.open(errFile) editor.gotoLine(errLine, errCol) + }).catch(error => { + if (error) return console.log(error) }) } } else { diff --git a/apps/remix-ide/src/app/ui/universal-dapp-ui.js b/apps/remix-ide/src/app/ui/universal-dapp-ui.js index 3573403b78..e3eb358090 100644 --- a/apps/remix-ide/src/app/ui/universal-dapp-ui.js +++ b/apps/remix-ide/src/app/ui/universal-dapp-ui.js @@ -14,6 +14,7 @@ var txFormat = remixLib.execution.txFormat const txHelper = remixLib.execution.txHelper var TreeView = require('./TreeView') var txCallBacks = require('./sendTxCallbacks') +const _paq = window._paq = window._paq || [] function UniversalDAppUI (blockchain, logCallback) { this.blockchain = blockchain @@ -243,6 +244,8 @@ UniversalDAppUI.prototype.runTransaction = function (lookupOnly, args, valArr, i outputOverride.appendChild(decoded) } } + const info = `${lookupOnly ? 'call' : args.funABI.type !== 'fallback' ? 'lowLevelInteracions' : 'transact'}_${this.blockchain.executionContext.executionContext}` + _paq.push(['trackEvent', 'udapp', info]) const params = args.funABI.type !== 'fallback' ? inputsValues : '' this.blockchain.runOrCallContractMethod( args.contractName, diff --git a/apps/remix-ide/src/lib/helper.js b/apps/remix-ide/src/lib/helper.js index ac3f71a0e5..2d8bb19cfb 100644 --- a/apps/remix-ide/src/lib/helper.js +++ b/apps/remix-ide/src/lib/helper.js @@ -36,14 +36,12 @@ module.exports = { async.whilst( () => { return exist }, (callback) => { - fileProvider.exists(name + counter + prefix + '.' + ext, (error, currentExist) => { - if (error) { - callback(error) - } else { - exist = currentExist - if (exist) counter = (counter | 0) + 1 - callback() - } + fileProvider.exists(name + counter + prefix + '.' + ext).then(currentExist => { + exist = currentExist + if (exist) counter = (counter | 0) + 1 + callback() + }).catch(error => { + if (error) console.log(error) }) }, (error) => { cb(error, name + counter + prefix + '.' + ext) } @@ -52,6 +50,27 @@ module.exports = { createNonClashingName (name, fileProvider, cb) { this.createNonClashingNameWithPrefix(name, fileProvider, '', cb) }, + async createNonClashingNameAsync (name, fileManager, prefix = '') { + if (!name) name = 'Undefined' + let counter = '' + let ext = 'sol' + const reg = /(.*)\.([^.]+)/g + const split = reg.exec(name) + if (split) { + name = split[1] + ext = split[2] + } + let exist = true + + do { + const isDuplicate = await fileManager.exists(name + counter + prefix + '.' + ext) + + if (isDuplicate) counter = (counter | 0) + 1 + else exist = false + } while (exist) + + return name + counter + prefix + '.' + ext + }, checkSpecialChars (name) { return name.match(/[:*?"<>\\'|]/) != null }, diff --git a/libs/remix-debug/src/Ethdebugger.ts b/libs/remix-debug/src/Ethdebugger.ts index 7f2ea7667a..3c492e1812 100644 --- a/libs/remix-debug/src/Ethdebugger.ts +++ b/libs/remix-debug/src/Ethdebugger.ts @@ -104,9 +104,10 @@ export class Ethdebugger { const stack = this.traceManager.getStackAt(step) const memory = this.traceManager.getMemoryAt(step) const address = this.traceManager.getCurrentCalledAddressAt(step) + const calldata = this.traceManager.getCallDataAt(step) try { const storageViewer = new StorageViewer({ stepIndex: step, tx: this.tx, address: address }, this.storageResolver, this.traceManager) - const locals = await localDecoder.solidityLocals(step, this.callTree, stack, memory, storageViewer, sourceLocation, null) + const locals = await localDecoder.solidityLocals(step, this.callTree, stack, memory, storageViewer, calldata, sourceLocation, null) if (locals['error']) { return callback(locals['error']) } diff --git a/libs/remix-debug/src/debugger/solidityLocals.ts b/libs/remix-debug/src/debugger/solidityLocals.ts index 64796a5f50..c7f25d01b8 100644 --- a/libs/remix-debug/src/debugger/solidityLocals.ts +++ b/libs/remix-debug/src/debugger/solidityLocals.ts @@ -62,6 +62,14 @@ export class DebuggerSolidityLocals { } catch (error) { next(error) } + }, + function getCallDataAt (stepIndex, next) { + try { + const calldata = self.traceManager.getCallDataAt(stepIndex) + next(null, calldata) + } catch (error) { + next(error) + } }], this.stepManager.currentStepIndex, (error, result) => { @@ -70,9 +78,10 @@ export class DebuggerSolidityLocals { } var stack = result[0].value var memory = result[1].value + var calldata = result[3].value try { var storageViewer = new StorageViewer({ stepIndex: this.stepManager.currentStepIndex, tx: this.tx, address: result[2].value }, this.storageResolver, this.traceManager) - solidityLocals(this.stepManager.currentStepIndex, this.internalTreeCall, stack, memory, storageViewer, sourceLocation, cursor).then((locals) => { + solidityLocals(this.stepManager.currentStepIndex, this.internalTreeCall, stack, memory, storageViewer, calldata, sourceLocation, cursor).then((locals) => { if (!cursor) { if (!locals['error']) { this.event.trigger('solidityLocals', [locals]) diff --git a/libs/remix-debug/src/solidity-decoder/internalCallTree.ts b/libs/remix-debug/src/solidity-decoder/internalCallTree.ts index acdeae4749..d84e023047 100644 --- a/libs/remix-debug/src/solidity-decoder/internalCallTree.ts +++ b/libs/remix-debug/src/solidity-decoder/internalCallTree.ts @@ -310,10 +310,10 @@ async function includeVariableDeclaration (tree, step, sourceLocation, scopeId, // } // input params if (inputs && inputs.parameters) { - functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, inputs.parameters.length, -1) + functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, inputs.parameters.length, -1) } // output params - if (outputs) addParams(outputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, 0, 1) + if (outputs) addParams(outputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, 0, 1) } } catch (error) { console.log(error) @@ -373,7 +373,8 @@ function extractFunctionDefinitions (ast, astWalker) { return ret } -function addParams (parameterList, tree, scopeId, states, contractName, sourceLocation, stackLength, stackPosition, dir) { +function addParams (parameterList, tree, scopeId, states, contractObj, sourceLocation, stackLength, stackPosition, dir) { + const contractName = contractObj.name const params = [] for (const inputParam in parameterList.parameters) { const param = parameterList.parameters[inputParam] @@ -386,7 +387,8 @@ function addParams (parameterList, tree, scopeId, states, contractName, sourceLo name: attributesName, type: parseType(param.typeDescriptions.typeString, states, contractName, location), stackDepth: stackDepth, - sourceLocation: sourceLocation + sourceLocation: sourceLocation, + abi: contractObj.contract.abi } params.push(attributesName) } diff --git a/libs/remix-debug/src/solidity-decoder/localDecoder.ts b/libs/remix-debug/src/solidity-decoder/localDecoder.ts index 89aba92a2a..9564efc93b 100644 --- a/libs/remix-debug/src/solidity-decoder/localDecoder.ts +++ b/libs/remix-debug/src/solidity-decoder/localDecoder.ts @@ -1,6 +1,6 @@ 'use strict' -export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, storageResolver, currentSourceLocation, cursor) { +export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, storageResolver, calldata, currentSourceLocation, cursor) { const scope = internalTreeCall.findScope(vmtraceIndex) if (!scope) { const error = { message: 'Can\'t display locals. reason: compilation result might not have been provided' } @@ -18,7 +18,7 @@ export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, mem anonymousIncr++ } try { - locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, cursor) + locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, calldata, cursor, variable) } catch (e) { console.log(e) locals[name] = '' diff --git a/libs/remix-debug/src/solidity-decoder/types/RefType.ts b/libs/remix-debug/src/solidity-decoder/types/RefType.ts index 97ce46a6a0..031d52f295 100644 --- a/libs/remix-debug/src/solidity-decoder/types/RefType.ts +++ b/libs/remix-debug/src/solidity-decoder/types/RefType.ts @@ -1,4 +1,5 @@ 'use strict' +import { ethers } from 'ethers' import { toBN } from './util' export class RefType { @@ -7,6 +8,7 @@ export class RefType { storageBytes typeName basicType + underlyingType constructor (storageSlots, storageBytes, typeName, location) { this.location = location @@ -33,7 +35,7 @@ export class RefType { * @param {Object} - storageResolver * @return {Object} decoded value */ - async decodeFromStack (stackDepth, stack, memory, storageResolver, cursor): Promise { + async decodeFromStack (stackDepth, stack, memory, storageResolver, calldata, cursor, variableDetails?): Promise { if (stack.length - 1 < stackDepth) { return { error: '', type: this.typeName } } @@ -49,6 +51,26 @@ export class RefType { } else if (this.isInMemory()) { offset = parseInt(offset, 16) return this.decodeFromMemoryInternal(offset, memory, cursor) + } else if (this.isInCallData()) { + calldata = calldata.length > 0 ? calldata[0] : '0x' + const ethersAbi = new ethers.utils.Interface(variableDetails.abi) + const fnSign = calldata.substr(0, 10) + const decodedData = ethersAbi.decodeFunctionData(ethersAbi.getFunction(fnSign), calldata) + let decodedValue = decodedData[variableDetails.name] + const isArray = Array.isArray(decodedValue) + if (isArray) { + decodedValue = decodedValue.map((el) => { + return { + value: el.toString(), + type: this.underlyingType.typeName + } + }) + } + return { + length: Array.isArray(decodedValue) ? '0x' + decodedValue.length.toString(16) : undefined, + value: decodedValue, + type: this.typeName + } } else { return { error: '', type: this.typeName } } @@ -84,4 +106,13 @@ export class RefType { isInMemory () { return this.location.indexOf('memory') === 0 } + + /** + * current type defined in storage + * + * @return {Bool} - return true if the type is defined in the storage + */ + isInCallData () { + return this.location.indexOf('calldata') === 0 + } } diff --git a/libs/remix-debug/src/solidity-decoder/types/StringType.ts b/libs/remix-debug/src/solidity-decoder/types/StringType.ts index 2c8f4cac60..8e31b3d17f 100644 --- a/libs/remix-debug/src/solidity-decoder/types/StringType.ts +++ b/libs/remix-debug/src/solidity-decoder/types/StringType.ts @@ -20,9 +20,9 @@ export class StringType extends DynamicByteArray { return format(decoded) } - async decodeFromStack (stackDepth, stack, memory) { + async decodeFromStack (stackDepth, stack, memory, calldata, variableDetails?) { try { - return await super.decodeFromStack(stackDepth, stack, memory, null, null) + return await super.decodeFromStack(stackDepth, stack, memory, null, calldata, variableDetails) } catch (e) { console.log(e) return '' diff --git a/libs/remix-debug/src/solidity-decoder/types/ValueType.ts b/libs/remix-debug/src/solidity-decoder/types/ValueType.ts index 61b4c65c54..63bd4ae0f0 100644 --- a/libs/remix-debug/src/solidity-decoder/types/ValueType.ts +++ b/libs/remix-debug/src/solidity-decoder/types/ValueType.ts @@ -43,7 +43,7 @@ export class ValueType { * @param {String} - memory * @return {Object} - decoded value */ - async decodeFromStack (stackDepth, stack, memory) { + async decodeFromStack (stackDepth, stack, memory, calldata, variableDetails?) { let value if (stackDepth >= stack.length) { value = this.decodeValue('') diff --git a/libs/remix-debug/test/decoder/localsTests/helper.ts b/libs/remix-debug/test/decoder/localsTests/helper.ts index 10537faee1..c041f4364a 100644 --- a/libs/remix-debug/test/decoder/localsTests/helper.ts +++ b/libs/remix-debug/test/decoder/localsTests/helper.ts @@ -22,13 +22,21 @@ export function decodeLocals (st, index, traceManager, callTree, verifier) { } catch (error) { callback(error) } + }, + function getCallDataAt (stepIndex, callback) { + try { + const result = traceManager.getCallDataAt(stepIndex) + callback(null, result) + } catch (error) { + callback(error) + } }], index, function (error, result) { if (error) { return st.fail(error) } - solidityLocals(index, callTree, result[0].value, result[1].value, {}, { start: 5000 }, null).then((locals) => { + solidityLocals(index, callTree, result[0].value, result[1].value, {}, result[2].value, { start: 5000 }, null).then((locals) => { verifier(locals) }) }) diff --git a/libs/remix-ui/checkbox/.babelrc b/libs/remix-ui/checkbox/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/checkbox/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/checkbox/.eslintrc b/libs/remix-ui/checkbox/.eslintrc new file mode 100644 index 0000000000..dae5c6feeb --- /dev/null +++ b/libs/remix-ui/checkbox/.eslintrc @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "../../../.eslintrc", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" + } +} diff --git a/libs/remix-ui/checkbox/README.md b/libs/remix-ui/checkbox/README.md new file mode 100644 index 0000000000..56f9b617b0 --- /dev/null +++ b/libs/remix-ui/checkbox/README.md @@ -0,0 +1,7 @@ +# remix-ui-checkbox + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test remix-ui-checkbox` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/remix-ui/checkbox/src/index.ts b/libs/remix-ui/checkbox/src/index.ts new file mode 100644 index 0000000000..27b694c6bd --- /dev/null +++ b/libs/remix-ui/checkbox/src/index.ts @@ -0,0 +1 @@ +export * from './lib/remix-ui-checkbox' diff --git a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.css b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx new file mode 100644 index 0000000000..5535a05971 --- /dev/null +++ b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx @@ -0,0 +1,47 @@ +import React from 'react' //eslint-disable-line +import './remix-ui-checkbox.css' + +/* eslint-disable-next-line */ +export interface RemixUiCheckboxProps { + onClick?: (event) => void + onChange?: (event) => void + label?: string + inputType?: string + name?: string + checked?: boolean + id?: string + itemName?: string + categoryId?: string +} + +export const RemixUiCheckbox = ({ + id, + label, + onClick, + inputType, + name, + checked, + onChange, + itemName, + categoryId +}: RemixUiCheckboxProps) => { + return ( +
+ + +
+ ) +} + +export default RemixUiCheckbox diff --git a/libs/remix-ui/checkbox/tsconfig.json b/libs/remix-ui/checkbox/tsconfig.json new file mode 100644 index 0000000000..6b65264565 --- /dev/null +++ b/libs/remix-ui/checkbox/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/remix-ui/checkbox/tsconfig.lib.json b/libs/remix-ui/checkbox/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/checkbox/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts b/libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts new file mode 100644 index 0000000000..8e3bc9f1c4 --- /dev/null +++ b/libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts @@ -0,0 +1,293 @@ +import React from 'react' +import { File } from '../types' +import { extractNameFromKey, extractParentFromKey } from '../utils' + +export const fetchDirectoryError = (error: any) => { + return { + type: 'FETCH_DIRECTORY_ERROR', + payload: error + } +} + +export const fetchDirectoryRequest = (promise: Promise) => { + return { + type: 'FETCH_DIRECTORY_REQUEST', + payload: promise + } +} + +export const fetchDirectorySuccess = (path: string, files: File[]) => { + return { + type: 'FETCH_DIRECTORY_SUCCESS', + payload: { path, files } + } +} + +export const fileSystemReset = () => { + return { + type: 'FILESYSTEM_RESET' + } +} + +const normalize = (parent, filesList, newInputType?: string): any => { + const folders = {} + const files = {} + + Object.keys(filesList || {}).forEach(key => { + key = key.replace(/^\/|\/$/g, '') // remove first and last slash + let path = key + path = path.replace(/^\/|\/$/g, '') // remove first and last slash + + if (filesList[key].isDirectory) { + folders[extractNameFromKey(key)] = { + path, + name: extractNameFromKey(path), + isDirectory: filesList[key].isDirectory + } + } else { + files[extractNameFromKey(key)] = { + path, + name: extractNameFromKey(path), + isDirectory: filesList[key].isDirectory + } + } + }) + + if (newInputType === 'folder') { + const path = parent + '/blank' + + folders[path] = { + path: path, + name: '', + isDirectory: true + } + } else if (newInputType === 'file') { + const path = parent + '/blank' + + files[path] = { + path: path, + name: '', + isDirectory: false + } + } + + return Object.assign({}, folders, files) +} + +const fetchDirectoryContent = async (provider, folderPath: string, newInputType?: string): Promise => { + return new Promise((resolve) => { + provider.resolveDirectory(folderPath, (error, fileTree) => { + if (error) console.error(error) + const files = normalize(folderPath, fileTree, newInputType) + + resolve({ [extractNameFromKey(folderPath)]: files }) + }) + }) +} + +export const fetchDirectory = (provider, path: string) => (dispatch: React.Dispatch) => { + const promise = fetchDirectoryContent(provider, path) + + dispatch(fetchDirectoryRequest(promise)) + promise.then((files) => { + dispatch(fetchDirectorySuccess(path, files)) + }).catch((error) => { + dispatch(fetchDirectoryError({ error })) + }) + return promise +} + +export const resolveDirectoryError = (error: any) => { + return { + type: 'RESOLVE_DIRECTORY_ERROR', + payload: error + } +} + +export const resolveDirectoryRequest = (promise: Promise) => { + return { + type: 'RESOLVE_DIRECTORY_REQUEST', + payload: promise + } +} + +export const resolveDirectorySuccess = (path: string, files: File[]) => { + return { + type: 'RESOLVE_DIRECTORY_SUCCESS', + payload: { path, files } + } +} + +export const resolveDirectory = (provider, path: string) => (dispatch: React.Dispatch) => { + const promise = fetchDirectoryContent(provider, path) + + dispatch(resolveDirectoryRequest(promise)) + promise.then((files) => { + dispatch(resolveDirectorySuccess(path, files)) + }).catch((error) => { + dispatch(resolveDirectoryError({ error })) + }) + return promise +} + +export const fetchProviderError = (error: any) => { + return { + type: 'FETCH_PROVIDER_ERROR', + payload: error + } +} + +export const fetchProviderRequest = (promise: Promise) => { + return { + type: 'FETCH_PROVIDER_REQUEST', + payload: promise + } +} + +export const fetchProviderSuccess = (provider: any) => { + return { + type: 'FETCH_PROVIDER_SUCCESS', + payload: provider + } +} + +export const fileAddedSuccess = (path: string, files) => { + return { + type: 'FILE_ADDED', + payload: { path, files } + } +} + +export const folderAddedSuccess = (path: string, files) => { + return { + type: 'FOLDER_ADDED', + payload: { path, files } + } +} + +export const fileRemovedSuccess = (path: string, removePath: string) => { + return { + type: 'FILE_REMOVED', + payload: { path, removePath } + } +} + +export const fileRenamedSuccess = (path: string, removePath: string, files) => { + return { + type: 'FILE_RENAMED', + payload: { path, removePath, files } + } +} + +export const init = (provider, workspaceName: string, plugin, registry) => (dispatch: React.Dispatch) => { + if (provider) { + provider.event.register('fileAdded', async (filePath) => { + if (extractParentFromKey(filePath) === '/.workspaces') return + const path = extractParentFromKey(filePath) || provider.workspace || provider.type || '' + const data = await fetchDirectoryContent(provider, path) + + dispatch(fileAddedSuccess(path, data)) + if (filePath.includes('_test.sol')) { + plugin.event.trigger('newTestFileCreated', [filePath]) + } + }) + provider.event.register('folderAdded', async (folderPath) => { + if (extractParentFromKey(folderPath) === '/.workspaces') return + const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || '' + const data = await fetchDirectoryContent(provider, path) + + dispatch(folderAddedSuccess(path, data)) + }) + provider.event.register('fileRemoved', async (removePath) => { + const path = extractParentFromKey(removePath) || provider.workspace || provider.type || '' + + dispatch(fileRemovedSuccess(path, removePath)) + }) + provider.event.register('fileRenamed', async (oldPath) => { + const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || '' + const data = await fetchDirectoryContent(provider, path) + + dispatch(fileRenamedSuccess(path, oldPath, data)) + }) + provider.event.register('fileExternallyChanged', async (path: string, file: { content: string }) => { + const config = registry.get('config').api + const editor = registry.get('editor').api + + if (config.get('currentFile') === path && editor.currentContent() !== file.content) { + if (provider.isReadOnly(path)) return editor.setText(file.content) + dispatch(displayNotification( + path + ' changed', + 'This file has been changed outside of Remix IDE.', + 'Replace by the new content', 'Keep the content displayed in Remix', + () => { + editor.setText(file.content) + } + )) + } + }) + provider.event.register('fileRenamedError', async () => { + dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel')) + }) + provider.event.register('rootFolderChanged', async () => { + workspaceName = provider.workspace || provider.type || '' + fetchDirectory(provider, workspaceName)(dispatch) + }) + dispatch(fetchProviderSuccess(provider)) + dispatch(setCurrentWorkspace(workspaceName)) + } else { + dispatch(fetchProviderError('No provider available')) + } +} + +export const setCurrentWorkspace = (name: string) => { + return { + type: 'SET_CURRENT_WORKSPACE', + payload: name + } +} + +export const addInputFieldSuccess = (path: string, files: File[]) => { + return { + type: 'ADD_INPUT_FIELD', + payload: { path, files } + } +} + +export const addInputField = (provider, type: string, path: string) => (dispatch: React.Dispatch) => { + const promise = fetchDirectoryContent(provider, path, type) + + promise.then((files) => { + dispatch(addInputFieldSuccess(path, files)) + }).catch((error) => { + console.error(error) + }) + return promise +} + +export const removeInputFieldSuccess = (path: string) => { + return { + type: 'REMOVE_INPUT_FIELD', + payload: { path } + } +} + +export const removeInputField = (path: string) => (dispatch: React.Dispatch) => { + return dispatch(removeInputFieldSuccess(path)) +} + +export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => { + return { + type: 'DISPLAY_NOTIFICATION', + payload: { title, message, labelOk, labelCancel, actionOk, actionCancel } + } +} + +export const hideNotification = () => { + return { + type: 'DISPLAY_NOTIFICATION' + } +} + +export const closeNotificationModal = () => (dispatch: React.Dispatch) => { + dispatch(hideNotification()) +} diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx index 17f78d79fc..fc8c89450a 100644 --- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx +++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line +import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint-disable-line // import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line @@ -7,6 +7,8 @@ import Gists from 'gists' import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line import { FileExplorerProps, File } from './types' +import { fileSystemReducer, fileSystemInitialState } from './reducers/fileSystem' +import { fetchDirectory, init, resolveDirectory, addInputField, removeInputField } from './actions/fileSystem' import * as helper from '../../../../../apps/remix-ide/src/lib/helper' import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' @@ -15,7 +17,7 @@ import './css/file-explorer.css' const queryParams = new QueryParams() export const FileExplorer = (props: FileExplorerProps) => { - const { filesProvider, name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props + const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props const [state, setState] = useState({ focusElement: [{ key: '', @@ -24,10 +26,51 @@ export const FileExplorer = (props: FileExplorerProps) => { focusPath: null, files: [], fileManager: null, - filesProvider, ctrlKey: false, newFileName: '', - actions: [], + actions: [{ + id: 'newFile', + name: 'New File', + type: ['folder'], + path: [], + extension: [], + pattern: [] + }, { + id: 'newFolder', + name: 'New Folder', + type: ['folder'], + path: [], + extension: [], + pattern: [] + }, { + id: 'rename', + name: 'Rename', + type: ['file', 'folder'], + path: [], + extension: [], + pattern: [] + }, { + id: 'delete', + name: 'Delete', + type: ['file', 'folder'], + path: [], + extension: [], + pattern: [] + }, { + id: 'pushChangesToGist', + name: 'Push changes to gist', + type: [], + path: [], + extension: [], + pattern: ['^browser/gists/([0-9]|[a-z])*$'] + }, { + id: 'run', + name: 'Run', + type: [], + path: [], + extension: ['.js'], + pattern: [] + }], focusContext: { element: null, x: null, @@ -60,8 +103,43 @@ export const FileExplorer = (props: FileExplorerProps) => { mouseOverElement: null, showContextMenu: false }) + const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState) const editRef = useRef(null) + useEffect(() => { + if (props.filesProvider) { + init(props.filesProvider, props.name, props.plugin, props.registry)(dispatch) + } + }, [props.filesProvider, props.name]) + + useEffect(() => { + const provider = fileSystem.provider.provider + + if (provider) { + fetchDirectory(provider, props.name)(dispatch) + } + }, [fileSystem.provider.provider, props.name]) + + useEffect(() => { + if (fileSystem.notification.message) { + modal(fileSystem.notification.title, fileSystem.notification.message, { + label: fileSystem.notification.labelOk, + fn: fileSystem.notification.actionOk + }, { + label: fileSystem.notification.labelCancel, + fn: fileSystem.notification.actionCancel + }) + } + }, [fileSystem.notification.message]) + + useEffect(() => { + if (fileSystem.files.expandPath.length > 0) { + setState(prevState => { + return { ...prevState, expandPath: [...new Set([...prevState.expandPath, ...fileSystem.files.expandPath])] } + }) + } + }, [fileSystem.files.expandPath]) + useEffect(() => { if (state.focusEdit.element) { setTimeout(() => { @@ -75,95 +153,13 @@ export const FileExplorer = (props: FileExplorerProps) => { useEffect(() => { (async () => { const fileManager = registry.get('filemanager').api - const files = await fetchDirectoryContent(name) - const actions = [{ - id: 'newFile', - name: 'New File', - type: ['folder'], - path: [], - extension: [], - pattern: [] - }, { - id: 'newFolder', - name: 'New Folder', - type: ['folder'], - path: [], - extension: [], - pattern: [] - }, { - id: 'rename', - name: 'Rename', - type: ['file', 'folder'], - path: [], - extension: [], - pattern: [] - }, { - id: 'delete', - name: 'Delete', - type: ['file', 'folder'], - path: [], - extension: [], - pattern: [] - }, { - id: 'pushChangesToGist', - name: 'Push changes to gist', - type: [], - path: [], - extension: [], - pattern: ['^browser/gists/([0-9]|[a-z])*$'] - }, { - id: 'run', - name: 'Run', - type: [], - path: [], - extension: ['.js'], - pattern: [] - }] setState(prevState => { - return { ...prevState, fileManager, files, actions, expandPath: [name] } + return { ...prevState, fileManager, expandPath: [name] } }) })() }, [name]) - useEffect(() => { - if (state.fileManager) { - filesProvider.event.register('fileExternallyChanged', fileExternallyChanged) - filesProvider.event.register('fileRenamedError', fileRenamedError) - filesProvider.event.register('rootFolderChanged', rootFolderChanged) - } - }, [state.fileManager]) - - useEffect(() => { - const { expandPath } = state - const expandFn = async () => { - let files = state.files - - for (let i = 0; i < expandPath.length; i++) { - files = await resolveDirectory(expandPath[i], files) - await setState(prevState => { - return { ...prevState, files } - }) - } - } - - if (expandPath && expandPath.length > 0) { - expandFn() - } - }, [state.expandPath]) - - useEffect(() => { - // unregister event to update state in callback - if (filesProvider.event.registered.fileAdded) filesProvider.event.unregister('fileAdded', fileAdded) - if (filesProvider.event.registered.folderAdded) filesProvider.event.unregister('folderAdded', folderAdded) - if (filesProvider.event.registered.fileRemoved) filesProvider.event.unregister('fileRemoved', fileRemoved) - if (filesProvider.event.registered.fileRenamed) filesProvider.event.unregister('fileRenamed', fileRenamed) - filesProvider.event.register('fileAdded', fileAdded) - filesProvider.event.register('folderAdded', folderAdded) - filesProvider.event.register('fileRemoved', fileRemoved) - filesProvider.event.register('fileRenamed', fileRenamed) - }, [state.files]) - useEffect(() => { if (focusRoot) { setState(prevState => { @@ -220,82 +216,6 @@ export const FileExplorer = (props: FileExplorerProps) => { } }, [state.modals]) - const resolveDirectory = async (folderPath, dir: File[], isChild = false): Promise => { - if (!isChild && (state.focusEdit.element === '/blank') && state.focusEdit.isNew && (dir.findIndex(({ path }) => path === '/blank') === -1)) { - dir = state.focusEdit.type === 'file' ? [...dir, { - path: state.focusEdit.element, - name: '', - isDirectory: false - }] : [{ - path: state.focusEdit.element, - name: '', - isDirectory: true - }, ...dir] - } - dir = await Promise.all(dir.map(async (file) => { - if (file.path === folderPath) { - if ((extractParentFromKey(state.focusEdit.element) === folderPath) && state.focusEdit.isNew) { - file.child = state.focusEdit.type === 'file' ? [...await fetchDirectoryContent(folderPath), { - path: state.focusEdit.element, - name: '', - isDirectory: false - }] : [{ - path: state.focusEdit.element, - name: '', - isDirectory: true - }, ...await fetchDirectoryContent(folderPath)] - } else { - file.child = await fetchDirectoryContent(folderPath) - } - return file - } else if (file.child) { - file.child = await resolveDirectory(folderPath, file.child, true) - return file - } else { - return file - } - })) - - return dir - } - - const fetchDirectoryContent = async (folderPath: string): Promise => { - return new Promise((resolve) => { - filesProvider.resolveDirectory(folderPath, (_error, fileTree) => { - const files = normalize(fileTree) - - resolve(files) - }) - }) - } - - const normalize = (filesList): File[] => { - const folders = [] - const files = [] - - Object.keys(filesList || {}).forEach(key => { - key = key.replace(/^\/|\/$/g, '') // remove first and last slash - let path = key - path = path.replace(/^\/|\/$/g, '') // remove first and last slash - - if (filesList[key].isDirectory) { - folders.push({ - path, - name: extractNameFromKey(path), - isDirectory: filesList[key].isDirectory - }) - } else { - files.push({ - path, - name: extractNameFromKey(path), - isDirectory: filesList[key].isDirectory - }) - } - }) - - return [...folders, ...files] - } - const extractNameFromKey = (key: string):string => { const keyPath = key.split('/') @@ -310,29 +230,23 @@ export const FileExplorer = (props: FileExplorerProps) => { return keyPath.join('/') } - const createNewFile = (newFilePath: string) => { + const createNewFile = async (newFilePath: string) => { const fileManager = state.fileManager try { - helper.createNonClashingName(newFilePath, filesProvider, async (error, newName) => { - if (error) { - modal('Create File Failed', error, { - label: 'Close', - fn: async () => {} - }, null) - } else { - const createFile = await fileManager.writeFile(newName, '') + const newName = await helper.createNonClashingNameAsync(newFilePath, fileManager) + const createFile = await fileManager.writeFile(newName, '') - if (!createFile) { - return toast('Failed to create file ' + newName) - } else { - await fileManager.open(newName) - setState(prevState => { - return { ...prevState, focusElement: [{ key: newName, type: 'file' }] } - }) - } - } - }) + if (!createFile) { + return toast('Failed to create file ' + newName) + } else { + const path = newName.indexOf(props.name + '/') === 0 ? newName.replace(props.name + '/', '') : newName + + await fileManager.open(path) + setState(prevState => { + return { ...prevState, focusElement: [{ key: newName, type: 'file' }] } + }) + } } catch (error) { return modal('File Creation Failed', typeof error === 'string' ? error : error.message, { label: 'Close', @@ -367,6 +281,8 @@ export const FileExplorer = (props: FileExplorerProps) => { } const deletePath = async (path: string) => { + const filesProvider = fileSystem.provider.provider + if (filesProvider.isReadOnly(path)) { return toast('cannot delete file. ' + name + ' is a read only explorer') } @@ -410,106 +326,8 @@ export const FileExplorer = (props: FileExplorerProps) => { } } - const removePath = (path: string, files: File[]): File[] => { - return files.map(file => { - if (file.path === path) { - return null - } else if (file.child) { - const childFiles = removePath(path, file.child) - - file.child = childFiles.filter(file => file) - return file - } else { - return file - } - }) - } - - const fileAdded = async (filePath: string) => { - const pathArr = filePath.split('/') - const expandPath = pathArr.map((path, index) => { - return [...pathArr.slice(0, index)].join('/') - }).filter(path => path && (path !== props.name)) - const files = await fetchDirectoryContent(props.name) - - setState(prevState => { - const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])] - - return { ...prevState, files, expandPath: uniquePaths } - }) - if (filePath.includes('_test.sol')) { - plugin.event.trigger('newTestFileCreated', [filePath]) - } - } - - const folderAdded = async (folderPath: string) => { - const pathArr = folderPath.split('/') - const expandPath = pathArr.map((path, index) => { - return [...pathArr.slice(0, index)].join('/') - }).filter(path => path && (path !== props.name)) - const files = await fetchDirectoryContent(props.name) - - setState(prevState => { - const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])] - - return { ...prevState, files, expandPath: uniquePaths } - }) - } - - const fileExternallyChanged = (path: string, file: { content: string }) => { - const config = registry.get('config').api - const editor = registry.get('editor').api - - if (config.get('currentFile') === path && editor.currentContent() !== file.content) { - if (filesProvider.isReadOnly(path)) return editor.setText(file.content) - modal(path + ' changed', 'This file has been changed outside of Remix IDE.', { - label: 'Replace by the new content', - fn: () => { - editor.setText(file.content) - } - }, { - label: 'Keep the content displayed in Remix', - fn: () => {} - }) - } - } - - const fileRemoved = (filePath) => { - const files = removePath(filePath, state.files) - const updatedFiles = files.filter(file => file) - - setState(prevState => { - return { ...prevState, files: updatedFiles } - }) - } - - const fileRenamed = async () => { - const files = await fetchDirectoryContent(props.name) - - setState(prevState => { - return { ...prevState, files, expandPath: [...prevState.expandPath] } - }) - } - - // register to event of the file provider - // files.event.register('fileRenamed', fileRenamed) - const fileRenamedError = (error: string) => { - modal('File Renamed Failed', error, { - label: 'Close', - fn: () => {} - }, null) - } - - // register to event of the file provider - // files.event.register('rootFolderChanged', rootFolderChanged) - const rootFolderChanged = async () => { - const files = await fetchDirectoryContent(name) - setState(prevState => { - return { ...prevState, files } - }) - } - const uploadFile = (target) => { + const filesProvider = fileSystem.provider.provider // TODO The file explorer is merely a view on the current state of // 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 @@ -552,8 +370,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } const name = `${parentFolder}/${file.name}` - filesProvider.exists(name, (error, exist) => { - if (error) console.log(error) + filesProvider.exists(name).then(exist => { if (!exist) { loadFile(name) } else { @@ -567,6 +384,8 @@ export const FileExplorer = (props: FileExplorerProps) => { fn: () => {} }) } + }).catch(error => { + if (error) console.log(error) }) }) } @@ -582,6 +401,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } const toGist = (id?: string) => { + const filesProvider = fileSystem.provider.provider const proccedResult = function (error, data) { if (error) { modal('Publish to gist Failed', 'Failed to manage gist: ' + error, { @@ -698,6 +518,8 @@ export const FileExplorer = (props: FileExplorerProps) => { } const runScript = async (path: string) => { + const filesProvider = fileSystem.provider.provider + filesProvider.get(path, (error, content: string) => { if (error) return console.log(error) plugin.call('scriptRunner', 'execute', content) @@ -737,6 +559,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } const handleClickFile = (path: string) => { + path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path state.fileManager.open(path) setState(prevState => { return { ...prevState, focusElement: [{ key: path, type: 'file' }] } @@ -759,6 +582,7 @@ export const FileExplorer = (props: FileExplorerProps) => { if (!state.expandPath.includes(path)) { expandPath = [...new Set([...state.expandPath, path])] + resolveDirectory(fileSystem.provider.provider, path)(dispatch) } else { expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))] } @@ -790,7 +614,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } const editModeOn = (path: string, type: string, isNew: boolean = false) => { - if (filesProvider.isReadOnly(path)) return + if (fileSystem.provider.provider.isReadOnly(path)) return setState(prevState => { return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } } }) @@ -802,11 +626,9 @@ export const FileExplorer = (props: FileExplorerProps) => { if (!content || (content.trim() === '')) { if (state.focusEdit.isNew) { - const files = removePath(state.focusEdit.element, state.files) - const updatedFiles = files.filter(file => file) - + removeInputField(parentFolder)(dispatch) setState(prevState => { - return { ...prevState, files: updatedFiles, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } + return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } }) } else { editRef.current.textContent = state.focusEdit.lastEdit @@ -829,12 +651,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } else { if (state.focusEdit.isNew) { state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) - const files = removePath(state.focusEdit.element, state.files) - const updatedFiles = files.filter(file => file) - - setState(prevState => { - return { ...prevState, files: updatedFiles } - }) + removeInputField(parentFolder)(dispatch) } else { const oldPath: string = state.focusEdit.element const oldName = extractNameFromKey(oldPath) @@ -851,9 +668,10 @@ export const FileExplorer = (props: FileExplorerProps) => { } const handleNewFileInput = async (parentFolder?: string) => { - if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name + if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name const expandPath = [...new Set([...state.expandPath, parentFolder])] + await addInputField(fileSystem.provider.provider, 'file', parentFolder)(dispatch) setState(prevState => { return { ...prevState, expandPath } }) @@ -861,10 +679,11 @@ export const FileExplorer = (props: FileExplorerProps) => { } const handleNewFolderInput = async (parentFolder?: string) => { - if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name + if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder) const expandPath = [...new Set([...state.expandPath, parentFolder])] + await addInputField(fileSystem.provider.provider, 'folder', parentFolder)(dispatch) setState(prevState => { return { ...prevState, expandPath } }) @@ -915,12 +734,16 @@ export const FileExplorer = (props: FileExplorerProps) => { } const renderFiles = (file: File, index: number) => { + if (!file || !file.path || typeof file === 'string' || typeof file === 'number' || typeof file === 'boolean') return const labelClass = state.focusEdit.element === file.path ? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1 ? 'bg-secondary' : state.mouseOverElement === file.path ? 'bg-light border' : (state.focusContext.element === file.path) && (state.focusEdit.element !== file.path) ? 'bg-light border' : '' const icon = helper.getPathIcon(file.path) + const spreadProps = { + onClick: (e) => e.stopPropagation() + } if (file.isDirectory) { return ( @@ -952,12 +775,12 @@ export const FileExplorer = (props: FileExplorerProps) => { }} > { - file.child ? { - file.child.map((file, index) => { - return renderFiles(file, index) + file.child ? { + Object.keys(file.child).map((key, index) => { + return renderFiles(file.child[key], index) }) } - : + : } ) @@ -965,7 +788,7 @@ export const FileExplorer = (props: FileExplorerProps) => { return ( { e.stopPropagation() @@ -1028,8 +851,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
{ - state.files.map((file, index) => { - return renderFiles(file, index) + fileSystem.files.files[props.name] && Object.keys(fileSystem.files.files[props.name]).map((key, index) => { + return renderFiles(fileSystem.files.files[props.name][key], index) }) } diff --git a/libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts b/libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts new file mode 100644 index 0000000000..dc1628ebf9 --- /dev/null +++ b/libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts @@ -0,0 +1,344 @@ +import * as _ from 'lodash' +import { extractNameFromKey } from '../utils' +interface Action { + type: string; + payload: Record; +} + +export const fileSystemInitialState = { + files: { + files: [], + expandPath: [], + workspaceName: null, + blankPath: null, + isRequesting: false, + isSuccessful: false, + error: null + }, + provider: { + provider: null, + isRequesting: false, + isSuccessful: false, + error: null + }, + notification: { + title: null, + message: null, + actionOk: () => {}, + actionCancel: () => {}, + labelOk: null, + labelCancel: null + } +} + +export const fileSystemReducer = (state = fileSystemInitialState, action: Action) => { + switch (action.type) { + case 'FETCH_DIRECTORY_REQUEST': { + return { + ...state, + files: { + ...state.files, + isRequesting: true, + isSuccessful: false, + error: null + } + } + } + case 'FETCH_DIRECTORY_SUCCESS': { + return { + ...state, + files: { + ...state.files, + files: action.payload.files, + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'FETCH_DIRECTORY_ERROR': { + return { + ...state, + files: { + ...state.files, + isRequesting: false, + isSuccessful: false, + error: action.payload + } + } + } + case 'RESOLVE_DIRECTORY_REQUEST': { + return { + ...state, + files: { + ...state.files, + isRequesting: true, + isSuccessful: false, + error: null + } + } + } + case 'RESOLVE_DIRECTORY_SUCCESS': { + return { + ...state, + files: { + ...state.files, + files: resolveDirectory(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'RESOLVE_DIRECTORY_ERROR': { + return { + ...state, + files: { + ...state.files, + isRequesting: false, + isSuccessful: false, + error: action.payload + } + } + } + case 'FETCH_PROVIDER_REQUEST': { + return { + ...state, + provider: { + ...state.provider, + isRequesting: true, + isSuccessful: false, + error: null + } + } + } + case 'FETCH_PROVIDER_SUCCESS': { + return { + ...state, + provider: { + ...state.provider, + provider: action.payload, + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'FETCH_PROVIDER_ERROR': { + return { + ...state, + provider: { + ...state.provider, + isRequesting: false, + isSuccessful: false, + error: action.payload + } + } + } + case 'SET_CURRENT_WORKSPACE': { + return { + ...state, + files: { + ...state.files, + workspaceName: action.payload + } + } + } + case 'ADD_INPUT_FIELD': { + return { + ...state, + files: { + ...state.files, + files: addInputField(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), + blankPath: action.payload.path, + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'REMOVE_INPUT_FIELD': { + return { + ...state, + files: { + ...state.files, + files: removeInputField(state.files.workspaceName, state.files.blankPath, state.files.files), + blankPath: null, + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'FILE_ADDED': { + return { + ...state, + files: { + ...state.files, + files: fileAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), + expandPath: [...new Set([...state.files.expandPath, action.payload.path])], + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'FOLDER_ADDED': { + return { + ...state, + files: { + ...state.files, + files: folderAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), + expandPath: [...new Set([...state.files.expandPath, action.payload.path])], + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'FILE_REMOVED': { + return { + ...state, + files: { + ...state.files, + files: fileRemoved(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files), + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'FILE_RENAMED': { + return { + ...state, + files: { + ...state.files, + files: fileRenamed(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files, action.payload.files), + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + case 'DISPLAY_NOTIFICATION': { + return { + ...state, + notification: { + title: action.payload.title, + message: action.payload.message, + actionOk: action.payload.actionOk || fileSystemInitialState.notification.actionOk, + actionCancel: action.payload.actionCancel || fileSystemInitialState.notification.actionCancel, + labelOk: action.payload.labelOk, + labelCancel: action.payload.labelCancel + } + } + } + case 'HIDE_NOTIFICATION': { + return { + ...state, + notification: fileSystemInitialState.notification + } + } + default: + throw new Error() + } +} + +const resolveDirectory = (root, path: string, files, content) => { + if (path === root) return { [root]: { ...content[root], ...files[root] } } + const pathArr: string[] = path.split('/').filter(value => value) + + if (pathArr[0] !== root) pathArr.unshift(root) + const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => { + return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur] + }, []) + + const prevFiles = _.get(files, _path) + + files = _.set(files, _path, { + isDirectory: true, + path, + name: extractNameFromKey(path), + child: { ...content[pathArr[pathArr.length - 1]], ...(prevFiles ? prevFiles.child : {}) } + }) + + return files +} + +const removePath = (root, path: string, pathName, files) => { + const pathArr: string[] = path.split('/').filter(value => value) + + if (pathArr[0] !== root) pathArr.unshift(root) + const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => { + return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur] + }, []) + const prevFiles = _.get(files, _path) + + prevFiles && prevFiles.child && prevFiles.child[pathName] && delete prevFiles.child[pathName] + files = _.set(files, _path, { + isDirectory: true, + path, + name: extractNameFromKey(path), + child: prevFiles ? prevFiles.child : {} + }) + + return files +} + +const addInputField = (root, path: string, files, content) => { + if (path === root) return { [root]: { ...content[root], ...files[root] } } + const result = resolveDirectory(root, path, files, content) + + return result +} + +const removeInputField = (root, path: string, files) => { + if (path === root) { + delete files[root][path + '/' + 'blank'] + return files + } + return removePath(root, path, path + '/' + 'blank', files) +} + +const fileAdded = (root, path: string, files, content) => { + return resolveDirectory(root, path, files, content) +} + +const folderAdded = (root, path: string, files, content) => { + return resolveDirectory(root, path, files, content) +} + +const fileRemoved = (root, path: string, removedPath: string, files) => { + if (path === root) { + delete files[root][removedPath] + + return files + } + return removePath(root, path, extractNameFromKey(removedPath), files) +} + +const fileRenamed = (root, path: string, removePath: string, files, content) => { + if (path === root) { + const allFiles = { [root]: { ...content[root], ...files[root] } } + + delete allFiles[root][extractNameFromKey(removePath) || removePath] + return allFiles + } + const pathArr: string[] = path.split('/').filter(value => value) + + if (pathArr[0] !== root) pathArr.unshift(root) + const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => { + return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur] + }, []) + const prevFiles = _.get(files, _path) + + delete prevFiles.child[extractNameFromKey(removePath)] + files = _.set(files, _path, { + isDirectory: true, + path, + name: extractNameFromKey(path), + child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child } + }) + + return files +} diff --git a/libs/remix-ui/file-explorer/src/lib/types/index.ts b/libs/remix-ui/file-explorer/src/lib/types/index.ts index 61c554dc3a..17d6975414 100644 --- a/libs/remix-ui/file-explorer/src/lib/types/index.ts +++ b/libs/remix-ui/file-explorer/src/lib/types/index.ts @@ -6,7 +6,7 @@ export interface FileExplorerProps { menuItems?: string[], plugin: any, focusRoot: boolean, - contextMenuItems: { name: string, type: string[], path: string[], extension: string[], pattern: string[] }[], + contextMenuItems: { id: string, name: string, type: string[], path: string[], extension: string[], pattern: string[] }[], displayInput?: boolean, externalUploads?: EventTarget & HTMLInputElement } diff --git a/libs/remix-ui/file-explorer/src/lib/utils/index.ts b/libs/remix-ui/file-explorer/src/lib/utils/index.ts new file mode 100644 index 0000000000..3db8f00902 --- /dev/null +++ b/libs/remix-ui/file-explorer/src/lib/utils/index.ts @@ -0,0 +1,13 @@ +export const extractNameFromKey = (key: string): string => { + const keyPath = key.split('/') + + return keyPath[keyPath.length - 1] +} + +export const extractParentFromKey = (key: string):string => { + if (!key) return + const keyPath = key.split('/') + keyPath.pop() + + return keyPath.join('/') +} diff --git a/libs/remix-ui/static-analyser/.babelrc b/libs/remix-ui/static-analyser/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/static-analyser/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/static-analyser/.eslintrc b/libs/remix-ui/static-analyser/.eslintrc new file mode 100644 index 0000000000..dae5c6feeb --- /dev/null +++ b/libs/remix-ui/static-analyser/.eslintrc @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "../../../.eslintrc", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" + } +} diff --git a/libs/remix-ui/static-analyser/README.md b/libs/remix-ui/static-analyser/README.md new file mode 100644 index 0000000000..7e4b95c0e5 --- /dev/null +++ b/libs/remix-ui/static-analyser/README.md @@ -0,0 +1,7 @@ +# remix-ui-static-analyser + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test remix-ui-static-analyser` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/remix-ui/static-analyser/src/index.ts b/libs/remix-ui/static-analyser/src/index.ts new file mode 100644 index 0000000000..86a00ccd14 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/index.ts @@ -0,0 +1 @@ +export * from './lib/remix-ui-static-analyser' diff --git a/libs/remix-ui/static-analyser/src/lib/Button/StaticAnalyserButton.tsx b/libs/remix-ui/static-analyser/src/lib/Button/StaticAnalyserButton.tsx new file mode 100644 index 0000000000..2a67c82cf8 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/Button/StaticAnalyserButton.tsx @@ -0,0 +1,21 @@ +import React from 'react' //eslint-disable-line + +interface StaticAnalyserButtonProps { + onClick: (event) => void + buttonText: string, + disabled?: boolean +} + +const StaticAnalyserButton = ({ + onClick, + buttonText, + disabled +}: StaticAnalyserButtonProps) => { + return ( + + ) +} + +export default StaticAnalyserButton diff --git a/libs/remix-ui/static-analyser/src/lib/ErrorRenderer.tsx b/libs/remix-ui/static-analyser/src/lib/ErrorRenderer.tsx new file mode 100644 index 0000000000..8221d3f694 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/ErrorRenderer.tsx @@ -0,0 +1,65 @@ +import React from 'react' //eslint-disable-line + +interface ErrorRendererProps { + message: any; + opt: any, + warningErrors: any + editor: any +} + +const ErrorRenderer = ({ message, opt, editor }: ErrorRendererProps) => { + const getPositionDetails = (msg: any) => { + const result = { } as Record + + // To handle some compiler warning without location like SPDX license warning etc + if (!msg.includes(':')) return { errLine: -1, errCol: -1, errFile: msg } + + // extract line / column + let position = msg.match(/^(.*?):([0-9]*?):([0-9]*?)?/) + result.errLine = position ? parseInt(position[2]) - 1 : -1 + result.errCol = position ? parseInt(position[3]) : -1 + + // extract file + position = msg.match(/^(https:.*?|http:.*?|.*?):/) + result.errFile = position ? position[1] : '' + return result + } + + const handlePointToErrorOnClick = (location, fileName) => { + editor.call('editor', 'discardHighlight') + editor.call('editor', 'highlight', location, fileName) + } + + if (!message) return + let position = getPositionDetails(message) + if (!position.errFile || (opt.errorType && opt.errorType === position.errFile)) { + // Updated error reported includes '-->' before file details + const errorDetails = message.split('-->') + // errorDetails[1] will have file details + if (errorDetails.length > 1) position = getPositionDetails(errorDetails[1]) + } + opt.errLine = position.errLine + opt.errCol = position.errCol + opt.errFile = position.errFile.trim() + const classList = opt.type === 'error' ? 'alert alert-danger' : 'alert alert-warning' + return ( +
+
+
+ +
+ handlePointToErrorOnClick(opt.location, opt.fileName)}> + {opt.name} + { opt.item.warning } + {opt.item.more + ? more + : + } + Pos: {opt.locationString} + +
+
+ ) +} + +export default ErrorRenderer diff --git a/libs/remix-ui/static-analyser/src/lib/actions/staticAnalysisActions.ts b/libs/remix-ui/static-analyser/src/lib/actions/staticAnalysisActions.ts new file mode 100644 index 0000000000..4f55437cb6 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/actions/staticAnalysisActions.ts @@ -0,0 +1,14 @@ +import React from 'react' //eslint-disable-line + +export const compilation = (analysisModule, dispatch) => { + if (analysisModule) { + analysisModule.on( + 'solidity', + 'compilationFinished', + (file, source, languageVersion, data) => { + if (languageVersion.indexOf('soljson') !== 0) return + dispatch({ type: 'compilationFinished', payload: { file, source, languageVersion, data } }) + } + ) + } +} diff --git a/libs/remix-ui/static-analyser/src/lib/reducers/staticAnalysisReducer.ts b/libs/remix-ui/static-analyser/src/lib/reducers/staticAnalysisReducer.ts new file mode 100644 index 0000000000..eb59ca7872 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/reducers/staticAnalysisReducer.ts @@ -0,0 +1,21 @@ +export const initialState = { + file: null, + source: null, + languageVersion: null, + data: null +} + +export const analysisReducer = (state, action) => { + switch (action.type) { + case 'compilationFinished': + return { + ...state, + file: action.payload.file, + source: action.payload.source, + languageVersion: action.payload.languageVersion, + data: action.payload.data + } + default: + return initialState + } +} diff --git a/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx new file mode 100644 index 0000000000..5bb91441af --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx @@ -0,0 +1,351 @@ +import React, { useEffect, useState, useReducer } from 'react' +import Button from './Button/StaticAnalyserButton' // eslint-disable-line +import remixLib from '@remix-project/remix-lib' +import _ from 'lodash' +import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line +import { RemixUiCheckbox } from '@remix-ui/checkbox' // eslint-disable-line +import ErrorRenderer from './ErrorRenderer' // eslint-disable-line +import { compilation } from './actions/staticAnalysisActions' +import { initialState, analysisReducer } from './reducers/staticAnalysisReducer' +const StaticAnalysisRunner = require('@remix-project/remix-analyzer').CodeAnalysis +const utils = remixLib.util + +/* eslint-disable-next-line */ +export interface RemixUiStaticAnalyserProps { + registry: any, + event: any, + analysisModule: any +} + +export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { + const [runner] = useState(new StaticAnalysisRunner()) + + const preProcessModules = (arr: any) => { + return arr.map((Item, i) => { + const itemObj = new Item() + itemObj._index = i + itemObj.categoryDisplayName = itemObj.category.displayName + itemObj.categoryId = itemObj.category.id + return itemObj + }) + } + + const groupedModules = utils.groupBy( + preProcessModules(runner.modules()), + 'categoryId' + ) + + const getIndex = (modules, array) => { + Object.values(modules).map((value: {_index}) => { + if (Array.isArray(value)) { + value.forEach((x) => { + array.push(x._index.toString()) + }) + } else { + array.push(value._index.toString()) + } + }) + } + + const groupedModuleIndex = (modules) => { + const indexOfCategory = [] + if (!_.isEmpty(modules)) { + getIndex(modules, indexOfCategory) + } + return indexOfCategory + } + const [autoRun, setAutoRun] = useState(true) + const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules)) + + const warningContainer = React.useRef(null) + const [warningState, setWarningState] = useState([]) + const [state, dispatch] = useReducer(analysisReducer, initialState) + + useEffect(() => { + compilation(props.analysisModule, dispatch) + }, []) + + useEffect(() => { + if (autoRun) { + if (state.data !== null) { + run(state.data, state.source, state.file) + } + } + return () => { } + }, [autoRun, categoryIndex, state]) + + const message = (name, warning, more, fileName, locationString) : string => { + return (` + + ${name} + ${warning} + ${more + ? (more) + : ( ) + } + Pos: ${locationString} + ` + ) + } + + const run = (lastCompilationResult, lastCompilationSource, currentFile) => { + if (state.data !== null) { + if (lastCompilationResult && categoryIndex.length > 0) { + let warningCount = 0 + const warningMessage = [] + + runner.run(lastCompilationResult, categoryIndex, results => { + results.map((result) => { + let moduleName + Object.keys(groupedModules).map(key => { + groupedModules[key].forEach(el => { + if (el.name === result.name) { + moduleName = groupedModules[key][0].categoryDisplayName + } + }) + }) + const warningErrors = [] + result.report.map((item) => { + let location: any = {} + let locationString = 'not available' + let column = 0 + let row = 0 + let fileName = currentFile + if (item.location) { + const split = item.location.split(':') + const file = split[2] + location = { + start: parseInt(split[0]), + length: parseInt(split[1]) + } + location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn( + location, + parseInt(file), + lastCompilationSource.sources, + lastCompilationResult.sources + ) + row = location.start.line + column = location.start.column + locationString = row + 1 + ':' + column + ':' + fileName = Object.keys(lastCompilationResult.contracts)[file] + } + warningCount++ + const msg = message(item.name, item.warning, item.more, fileName, locationString) + const options = { + type: 'warning', + useSpan: true, + errFile: fileName, + fileName, + errLine: row, + errCol: column, + item: item, + name: result.name, + locationString, + more: item.more, + location: location + } + warningErrors.push(options) + warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName }) + }) + }) + const resultArray = [] + warningMessage.map(x => { + resultArray.push(x) + }) + function groupBy (objectArray, property) { + return objectArray.reduce((acc, obj) => { + const key = obj[property] + if (!acc[key]) { + acc[key] = [] + } + // Add object to list for given key's value + acc[key].push(obj) + return acc + }, {}) + } + + const groupedCategory = groupBy(resultArray, 'warningModuleName') + setWarningState(groupedCategory) + }) + if (categoryIndex.length > 0) { + props.event.trigger('staticAnaysisWarning', [warningCount]) + } + } else { + if (categoryIndex.length) { + warningContainer.current.innerText = 'No compiled AST available' + } + props.event.trigger('staticAnaysisWarning', [-1]) + } + } + } + + const handleCheckAllModules = (groupedModules) => { + const index = groupedModuleIndex(groupedModules) + if (index.every(el => categoryIndex.includes(el))) { + setCategoryIndex( + categoryIndex.filter((el) => { + return !index.includes(el) + }) + ) + } else { + setCategoryIndex(_.uniq([...categoryIndex, ...index])) + } + } + + const handleCheckOrUncheckCategory = (category) => { + const index = groupedModuleIndex(category) + if (index.every(el => categoryIndex.includes(el))) { + setCategoryIndex( + categoryIndex.filter((el) => { + return !index.includes(el) + }) + ) + } else { + setCategoryIndex(_.uniq([...categoryIndex, ...index])) + } + } + + const handleAutoRun = () => { + if (autoRun) { + setAutoRun(false) + } else { + setAutoRun(true) + } + } + + const handleCheckSingle = (event, _index) => { + _index = _index.toString() + if (categoryIndex.includes(_index)) { + setCategoryIndex(categoryIndex.filter(val => val !== _index)) + } else { + setCategoryIndex(_.uniq([...categoryIndex, _index])) + } + } + + const categoryItem = (categoryId, item, i) => { + return ( +
+ handleCheckSingle(event, item._index)} + checked={categoryIndex.includes(item._index.toString())} + onChange={() => {}} + /> +
+ ) + } + + const categorySection = (category, categoryId, i) => { + return ( +
+
+ + + {category[0].categoryDisplayName} + + } + expand={false} + > +
+ handleCheckOrUncheckCategory(category)} id={categoryId} inputType="checkbox" label={`Select ${category[0].categoryDisplayName}`} name='checkCategoryEntry' checked={category.map(x => x._index.toString()).every(el => categoryIndex.includes(el))} onChange={() => {}}/> +
+
+ {category.map((item, i) => { + return ( + categoryItem(categoryId, item, i) + ) + })} +
+
+
+
+
+ ) + } + + return ( +
+
+
+ { + return (value.map(x => { + return x._index.toString() + })) + }).flat().every(el => categoryIndex.includes(el))} + label="Select all" + onClick={() => handleCheckAllModules(groupedModules)} + onChange={() => {}} + /> + {}} + /> +
+
+
+ {Object.keys(groupedModules).map((categoryId, i) => { + const category = groupedModules[categoryId] + return ( + categorySection(category, categoryId, i) + ) + }) + } +
+
+ last results for: + + {state.file} + +
+ { categoryIndex.length > 0 && Object.entries(warningState).length > 0 && +
+
+ { + (Object.entries(warningState).map((element) => ( + <> + {element[0]} + {element[1].map(x => ( + x.hasWarning ? ( +
+ +
+ + ) : null + ))} + + ))) + } +
+
+ } +
+ ) +} + +export default RemixUiStaticAnalyser diff --git a/libs/remix-ui/static-analyser/tsconfig.json b/libs/remix-ui/static-analyser/tsconfig.json new file mode 100644 index 0000000000..6b65264565 --- /dev/null +++ b/libs/remix-ui/static-analyser/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/remix-ui/static-analyser/tsconfig.lib.json b/libs/remix-ui/static-analyser/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/static-analyser/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/remixd/src/services/remixdClient.ts b/libs/remixd/src/services/remixdClient.ts index 09f78b7dec..31588628ea 100644 --- a/libs/remixd/src/services/remixdClient.ts +++ b/libs/remixd/src/services/remixdClient.ts @@ -85,11 +85,10 @@ export class RemixdClient extends PluginClient { } } - set (args: SharedFolderArgs): Promise { + set (args: SharedFolderArgs) { try { return new Promise((resolve, reject) => { if (this.readOnly) return reject(new Error('Cannot write file: read-only mode selected')) - const isFolder = args.path.endsWith('/') const path = utils.absolutePath(args.path, this.currentSharedFolder) const exists = fs.existsSync(path) @@ -99,31 +98,25 @@ export class RemixdClient extends PluginClient { return reject(new Error('trying to write "undefined" ! stopping.')) } this.trackDownStreamUpdate[path] = path - if (isFolder) { - fs.mkdirp(path).then(() => { - let splitPath = args.path.split('/') - - splitPath = splitPath.filter(dir => dir) - const dir = '/' + splitPath.join('/') - - this.emit('folderAdded', dir) - resolve() - }).catch((e: Error) => reject(e)) + if (!exists && args.path.indexOf('/') !== -1) { + // the last element is the filename and we should remove it + this.createDir({ path: args.path.substr(0, args.path.lastIndexOf('/')) }) + } + try { + fs.writeFile(path, args.content, 'utf8', (error: Error) => { + if (error) { + console.log(error) + return reject(error) + } + resolve(true) + }) + } catch (e) { + return reject(e) + } + if (!exists) { + this.emit('fileAdded', args.path) } else { - fs.ensureFile(path).then(() => { - fs.writeFile(path, args.content, 'utf8', (error: Error) => { - if (error) { - console.log(error) - return reject(error) - } - resolve() - }) - }).catch((e: Error) => reject(e)) - if (!exists) { - this.emit('fileAdded', args.path) - } else { - this.emit('fileChanged', args.path) - } + this.emit('fileChanged', args.path) } }) } catch (error) { @@ -131,24 +124,22 @@ export class RemixdClient extends PluginClient { } } - createDir (args: SharedFolderArgs): Promise { + createDir (args: SharedFolderArgs) { try { return new Promise((resolve, reject) => { if (this.readOnly) return reject(new Error('Cannot create folder: read-only mode selected')) - const path = utils.absolutePath(args.path, this.currentSharedFolder) - const exists = fs.existsSync(path) - - if (exists && !isRealPath(path)) return reject(new Error('')) - this.trackDownStreamUpdate[path] = path - fs.mkdirp(path).then(() => { - let splitPath = args.path.split('/') - - splitPath = splitPath.filter(dir => dir) - const dir = '/' + splitPath.join('/') - - this.emit('folderAdded', dir) - resolve() - }).catch((e: Error) => reject(e)) + const paths = args.path.split('/').filter(value => value) + if (paths.length && paths[0] === '') paths.shift() + let currentCheck = '' + paths.forEach((value) => { + currentCheck = currentCheck ? currentCheck + '/' + value : value + const path = utils.absolutePath(currentCheck, this.currentSharedFolder) + if (!fs.existsSync(path)) { + fs.mkdirp(path) + this.emit('folderAdded', currentCheck) + } + }) + resolve(true) }) } catch (error) { throw new Error(error) diff --git a/nx.json b/nx.json index 5cf047d9cc..a63c015d64 100644 --- a/nx.json +++ b/nx.json @@ -95,6 +95,12 @@ }, "remix-ui-workspace": { "tags": [] + }, + "remix-ui-static-analyser": { + "tags": [] + }, + "remix-ui-checkbox": { + "tags": [] } } } diff --git a/package-lock.json b/package-lock.json index 4f92e09fa8..d74204bd8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25139,9 +25139,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.15", diff --git a/package.json b/package.json index 2587e3bfbf..0e3b262c5e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "workspace-schematic": "nx workspace-schematic", "dep-graph": "nx dep-graph", "help": "nx help", - "lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace", + "lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox", "build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd", "test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd", "publish:libs": "npm run build:libs & lerna publish --skip-git & npm run bumpVersion:libs", @@ -159,7 +159,9 @@ "isbinaryfile": "^3.0.2", "jquery": "^3.3.1", "jszip": "^3.6.0", + "lodash": "^4.17.21", "latest-version": "^5.1.0", + "lodash": "^4.17.21", "merge": "^1.2.0", "npm-install-version": "^6.0.2", "react": "16.13.1", diff --git a/tsconfig.json b/tsconfig.json index c2a383e9de..75db5bc6ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "target": "es2015", "module": "commonjs", "typeRoots": ["node_modules/@types"], - "lib": ["es2017", "dom"], + "lib": ["es2017", "es2019", "dom"], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", @@ -38,7 +38,9 @@ "@remix-ui/modal-dialog": ["libs/remix-ui/modal-dialog/src/index.ts"], "@remix-ui/toaster": ["libs/remix-ui/toaster/src/index.ts"], "@remix-ui/file-explorer": ["libs/remix-ui/file-explorer/src/index.ts"], - "@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"] + "@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"], + "@remix-ui/static-analyser": ["libs/remix-ui/static-analyser/src/index.ts"], + "@remix-ui/checkbox": ["libs/remix-ui/checkbox/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 10793e13b3..5d2bf912c0 100644 --- a/workspace.json +++ b/workspace.json @@ -725,6 +725,41 @@ } } } + }, + "remix-ui-static-analyser": { + "root": "libs/remix-ui/static-analyser", + "sourceRoot": "libs/remix-ui/static-analyser/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "tsConfig": ["libs/remix-ui/static-analyser/tsconfig.lib.json"], + "exclude": [ + "**/node_modules/**", + "!libs/remix-ui/static-analyser/**/*" + ] + } + } + } + }, + "remix-ui-checkbox": { + "root": "libs/remix-ui/checkbox", + "sourceRoot": "libs/remix-ui/checkbox/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "tsConfig": ["libs/remix-ui/checkbox/tsconfig.lib.json"], + "exclude": ["**/node_modules/**", "!libs/remix-ui/checkbox/**/*"] + } + } + } } }, "cli": {