Merge branch 'master' into bunsenstraat-patch-1

pull/5370/head
EthereumRemix 4 years ago committed by GitHub
commit 5c968948be
  1. 4
      apps/debugger/.babelrc
  2. 16
      apps/debugger/.browserslistrc
  3. 17
      apps/debugger/src/app/app.tsx
  4. 157
      apps/debugger/src/app/debugger-api.ts
  5. 28
      apps/debugger/src/app/debugger.ts
  6. 0
      apps/debugger/src/assets/.gitkeep
  7. 3
      apps/debugger/src/environments/environment.prod.ts
  8. 6
      apps/debugger/src/environments/environment.ts
  9. BIN
      apps/debugger/src/favicon.ico
  10. 15
      apps/debugger/src/index.html
  11. 1
      apps/debugger/src/index.ts
  12. 9
      apps/debugger/src/main.tsx
  13. 7
      apps/debugger/src/polyfills.ts
  14. 1
      apps/debugger/src/styles.css
  15. 9
      apps/debugger/tsconfig.app.json
  16. 16
      apps/debugger/tsconfig.json
  17. 15
      apps/debugger/tsconfig.spec.json
  18. 17
      apps/debugger/webpack.config.js
  19. 26
      apps/remix-ide-e2e/src/commands/addFile.ts
  20. 2
      apps/remix-ide-e2e/src/commands/debugTransaction.ts
  21. 4
      apps/remix-ide-e2e/src/commands/getModalBody.ts
  22. 1
      apps/remix-ide-e2e/src/commands/goToVMTraceStep.ts
  23. 4
      apps/remix-ide-e2e/src/commands/openFile.ts
  24. 16
      apps/remix-ide-e2e/src/commands/removeFile.ts
  25. 13
      apps/remix-ide-e2e/src/commands/renamePath.ts
  26. 3
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  27. 2
      apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts
  28. 7
      apps/remix-ide-e2e/src/tests/debugger.test.ts
  29. 20
      apps/remix-ide-e2e/src/tests/defaultLayout.test.ts
  30. 25
      apps/remix-ide-e2e/src/tests/editor.test.ts
  31. 76
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  32. 11
      apps/remix-ide-e2e/src/tests/fileManager_api.test.ts
  33. 2
      apps/remix-ide-e2e/src/tests/generalSettings.test.ts
  34. 33
      apps/remix-ide-e2e/src/tests/gist.test.ts
  35. 18
      apps/remix-ide-e2e/src/tests/publishContract.test.ts
  36. 2
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  37. 1
      apps/remix-ide-e2e/src/tests/runAndDeploy.ts
  38. 6
      apps/remix-ide-e2e/src/tests/solidityImport.test.ts
  39. 5
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  40. 2
      apps/remix-ide-e2e/src/tests/usingWebWorker.test.ts
  41. 35
      apps/remix-ide-e2e/src/tests/verticalIconsPanel.test.ts
  42. 6
      apps/remix-ide-e2e/src/types/index.d.ts
  43. 7
      apps/remix-ide/src/app.js
  44. 6
      apps/remix-ide/src/app/compiler/compiler-sourceVerifier-fetchAndCompile.js
  45. 15
      apps/remix-ide/src/app/components/local-plugin.js
  46. 26
      apps/remix-ide/src/app/components/vertical-icons.js
  47. 15
      apps/remix-ide/src/app/editor/editor.js
  48. 6
      apps/remix-ide/src/app/files/fileManager.js
  49. 7
      apps/remix-ide/src/app/files/remixDProvider.js
  50. 7
      apps/remix-ide/src/app/files/remixd-handle.js
  51. 117
      apps/remix-ide/src/app/panels/file-panel.js
  52. 56
      apps/remix-ide/src/app/panels/styles/file-panel-styles.css
  53. 2
      apps/remix-ide/src/app/panels/tab-proxy.js
  54. 91
      apps/remix-ide/src/app/tabs/debugger-tab.js
  55. 2
      apps/remix-ide/src/app/tabs/test-tab.js
  56. 18
      apps/remix-ide/src/app/ui/contextMenu.js
  57. 12
      apps/remix-ide/src/app/ui/modaldialog.js
  58. 2
      apps/remix-ide/src/lib/offsetToLineColumnConverter.js
  59. 26
      apps/remix-ide/src/remixAppManager.js
  60. 6
      libs/remix-debug/src/debugger/debugger.ts
  61. 2
      libs/remix-lib/src/eventManager.ts
  62. 2
      libs/remix-ui/debugger-ui/src/index.ts
  63. 64
      libs/remix-ui/debugger-ui/src/lib/DebuggerAPI.ts
  64. 53
      libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx
  65. 66
      libs/remix-ui/debugger-ui/src/lib/idebugger-api.ts
  66. 4
      libs/remix-ui/file-explorer/.babelrc
  67. 19
      libs/remix-ui/file-explorer/.eslintrc
  68. 7
      libs/remix-ui/file-explorer/README.md
  69. 1
      libs/remix-ui/file-explorer/src/index.ts
  70. 28
      libs/remix-ui/file-explorer/src/lib/css/file-explorer-context-menu.css
  71. 56
      libs/remix-ui/file-explorer/src/lib/css/file-explorer.css
  72. 81
      libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx
  73. 97
      libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx
  74. 947
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  75. 41
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  76. 16
      libs/remix-ui/file-explorer/tsconfig.json
  77. 13
      libs/remix-ui/file-explorer/tsconfig.lib.json
  78. 90
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  79. 15
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  80. 10
      libs/remix-ui/toaster/src/lib/toaster.tsx
  81. 261
      libs/remix-ui/tree-view/.eslintrc
  82. 2
      libs/remix-ui/tree-view/jest.config.js
  83. 4
      libs/remix-ui/tree-view/src/index.ts
  84. 4
      libs/remix-ui/tree-view/src/lib/remix-ui-tree-view.tsx
  85. 12
      libs/remix-ui/tree-view/src/lib/tree-view-item/tree-view-item.tsx
  86. 19
      libs/remix-ui/tree-view/src/types/index.ts
  87. 26
      libs/remixd/src/services/remixdClient.ts
  88. 6
      nx.json
  89. 1005
      package-lock.json
  90. 12
      package.json
  91. 4
      tsconfig.json
  92. 65
      tslint.json
  93. 88
      workspace.json

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -0,0 +1,16 @@
# This file is used by:
# 1. autoprefixer to adjust CSS to support the below specified browsers
# 2. babel preset-env to adjust included polyfills
#
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.

@ -0,0 +1,17 @@
import React, { useState, useEffect } from 'react';
import { DebuggerUI } from '@remix-ui/debugger-ui' // eslint-disable-line
import { DebuggerClientApi } from './debugger'
const remix = new DebuggerClientApi()
export const App = () => {
return (
<div className="debugger">
<DebuggerUI debuggerAPI={remix} />
</div>
);
};
export default App;

@ -0,0 +1,157 @@
import Web3 from 'web3'
import remixDebug, { TransactionDebugger as Debugger } from '@remix-project/remix-debug'
import { CompilationOutput, Sources } from '@remix-ui/debugger-ui'
import type { CompilationResult } from '@remix-project/remix-solidity-ts'
export const DebuggerApiMixin = (Base) => class extends Base {
initDebuggerApi () {
this.debugHash = null
const self = this
this.web3Provider = {
sendAsync(payload, callback) {
self.call('web3Provider', 'sendAsync', payload)
.then(result => callback(null, result))
.catch(e => callback(e))
}
}
this._web3 = new Web3(this.web3Provider)
this.offsetToLineColumnConverter = {
async offsetToLineColumn (rawLocation, file, sources, asts) {
return await self.call('offsetToLineColumnConverter', 'offsetToLineColumn', rawLocation, file, sources, asts)
}
}
}
// on()
// call()
// onDebugRequested()
// onRemoveHighlights()
web3 () {
return this._web3
}
async discardHighlight () {
await this.call('editor', 'discardHighlight')
}
async highlight (lineColumnPos, path) {
await this.call('editor', 'highlight', lineColumnPos, path)
}
async getFile (path) {
return await this.call('fileManager', 'getFile', path)
}
async setFile (path, content) {
await this.call('fileManager', 'setFile', path, content)
}
onBreakpointCleared (listener) {
this.onBreakpointClearedListener = listener
}
onBreakpointAdded (listener) {
this.onBreakpointAddedListener = listener
}
onEditorContentChanged (listener) {
this.onEditorContentChangedListener = listener
}
onDebugRequested (listener) {
this.onDebugRequestedListener = listener
}
onRemoveHighlights (listener) {
this.onRemoveHighlightsListener = listener
}
async fetchContractAndCompile (address, receipt) {
const target = (address && remixDebug.traceHelper.isContractCreation(address)) ? receipt.contractAddress : address
const targetAddress = target || receipt.contractAddress || receipt.to
const codeAtAddress = await this._web3.eth.getCode(targetAddress)
const output = await this.call('fetchAndCompile', 'resolve', targetAddress, codeAtAddress, 'browser/.debug')
return new CompilerAbstract(output.languageversion, output.data, output.source)
}
async getDebugWeb3 () {
let web3
let network
try {
network = await this.call('network', 'detectNetwork')
} catch (e) {
web3 = this.web3()
}
if (!web3) {
const webDebugNode = remixDebug.init.web3DebugNode(network.name)
web3 = !webDebugNode ? this.web3() : webDebugNode
}
remixDebug.init.extendWeb3(web3)
return web3
}
async getTrace (hash) {
if (!hash) return
const web3 = await this.getDebugWeb3()
const currentReceipt = await web3.eth.getTransactionReceipt(hash)
const debug = new Debugger({
web3,
offsetToLineColumnConverter: this.offsetToLineColumnConverter,
compilationResult: async (address) => {
try {
return await this.fetchContractAndCompile(address, currentReceipt)
} catch (e) {
console.error(e)
}
return null
},
debugWithGeneratedSources: false
})
return await debug.debugger.traceManager.getTrace(hash)
}
debug (hash) {
this.debugHash = hash
this.onDebugRequestedListener(hash)
}
onActivation () {
this.on('editor', 'breakpointCleared', (fileName, row) => this.onBreakpointClearedListener(fileName, row))
this.on('editor', 'breakpointAdded', (fileName, row) => this.onBreakpointAddedListener(fileName, row))
this.on('editor', 'contentChanged', () => this.onEditorContentChangedListener())
}
onDeactivation () {
this.onRemoveHighlightsListener()
this.off('editor', 'breakpointCleared')
this.off('editor', 'breakpointAdded')
this.off('editor', 'contentChanged')
}
}
export class CompilerAbstract implements CompilationOutput { // this is a subset of /remix-ide/src/app/compiler/compiler-abstract.js
languageversion
data
source
constructor (languageversion: string, data: CompilationResult, source: { sources: Sources, target: string }) {
this.languageversion = languageversion
this.data = data
this.source = source // source code
}
getSourceName (fileIndex) {
if (this.data && this.data.sources) {
return Object.keys(this.data.sources)[fileIndex]
} else if (Object.keys(this.source.sources).length === 1) {
// if we don't have ast, we return the only one filename present.
const sourcesArray = Object.keys(this.source.sources)
return sourcesArray[0]
}
return null
}
}

@ -0,0 +1,28 @@
import { PluginClient } from "@remixproject/plugin";
import { createClient } from "@remixproject/plugin-webview";
import { IDebuggerApi, RawLocation, Sources, Asts, LineColumnLocation,
onBreakpointClearedListener, onBreakpointAddedListener, onEditorContentChanged, TransactionReceipt } from '@remix-ui/debugger-ui'
import { DebuggerApiMixin, CompilerAbstract} from './debugger-api'
export class DebuggerClientApi extends DebuggerApiMixin(PluginClient) {
constructor () {
super()
createClient(this as any)
this.initDebuggerApi()
}
offsetToLineColumnConverter: IDebuggerApi['offsetToLineColumnConverter']
debugHash: string
debugHashRequest: number
removeHighlights: boolean
onBreakpointCleared: (listener: onBreakpointClearedListener) => void
onBreakpointAdded: (listener: onBreakpointAddedListener) => void
onEditorContentChanged: (listener: onEditorContentChanged) => void
discardHighlight: () => Promise<void>
highlight: (lineColumnPos: LineColumnLocation, path: string) => Promise<void>
fetchContractAndCompile: (address: string, currentReceipt: TransactionReceipt) => Promise<CompilerAbstract>
getFile: (path: string) => Promise<string>
setFile: (path: string, content: string) => Promise<void>
getDebugWeb3: () => any // returns an instance of web3.js
}

@ -0,0 +1,3 @@
export const environment = {
production: true
};

@ -0,0 +1,6 @@
// This file can be replaced during build by using the `fileReplacements` array.
// When building for production, this file is replaced with `environment.prod.ts`.
export const environment = {
production: false
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Debugger</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"/>
</head>
<body>
<div id="root"></div>
</body>
</html>

@ -0,0 +1 @@
export * from './app/debugger-api';

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/app';
ReactDOM.render(
<App />,
document.getElementById('root')
);

@ -0,0 +1,7 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable';
import 'regenerator-runtime/runtime';

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"types": ["node", "jest"],
"resolveJsonModule": true
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx",
"**/*.d.ts"
]
}

@ -0,0 +1,17 @@
const nxWebpack = require('@nrwl/react/plugins/webpack')
module.exports = config => {
const nxWebpackConfig = nxWebpack(config)
return {
...nxWebpackConfig,
node: {
fs: 'empty',
tls: 'empty',
readline: 'empty',
net: 'empty',
module: 'empty',
child_process: 'empty'
}
}
}

@ -14,21 +14,17 @@ class AddFile extends EventEmitter {
}
function addFile (browser: NightwatchBrowser, name: string, content: NightwatchContractContent, done: VoidFunction) {
browser.clickLaunchIcon('udapp').clickLaunchIcon('fileExplorers').click('.newFile')
.waitForElementVisible('#modal-dialog')
.perform((client, done) => {
browser.execute(function (fileName) {
if (fileName !== 'Untitled.sol') {
document.querySelector('#modal-dialog #prompt_text').setAttribute('value', fileName)
}
const elem = document.querySelector('#modal-footer-ok') as HTMLElement
elem.click()
}, [name], function (result) {
console.log(result)
done()
})
})
browser.clickLaunchIcon('udapp')
.clickLaunchIcon('fileExplorers')
.click('li[data-id="treeViewLitreeViewItembrowser/README.txt"]') // focus on root directory
.click('.newFile')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/blank"]')
// .scrollAndClick('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items')
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', name)
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', browser.Keys.ENTER)
.pause(2000)
.waitForElementVisible(`li[data-id="treeViewLitreeViewItembrowser/${name}"]`)
.click(`li[data-id="treeViewLitreeViewItembrowser/${name}"]`)
.setEditorValue(content.content)
.pause(1000)
.perform(function () {

@ -19,7 +19,7 @@ function checkStyle (browser: NightwatchBrowser, index: number, callback: VoidFu
debugBtn && debugBtn.click()
}, [index], function () {
callback()
browser.waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]').perform(() => callback())
})
}

@ -3,8 +3,8 @@ import EventEmitter from "events"
class GetModalBody extends EventEmitter {
command (this: NightwatchBrowser, callback: (value: string, cb: VoidFunction) => void) {
this.api.waitForElementVisible('.modal-body')
.getText('.modal-body', (result) => {
this.api.waitForElementPresent('.modal-body')
.getText('#modal-dialog', (result) => {
console.log(result)
const value = typeof result.value === 'string' ? result.value : null

@ -14,6 +14,7 @@ function goToVMtraceStep (browser: NightwatchBrowser, step: number, incr: number
browser.execute(function () {
return document.querySelector('#stepdetail').innerHTML
}, [], function (result) {
console.log('goToVMtraceStep', result)
if (typeof result.value === 'string' && ( result.value.indexOf('vm trace step:') !== -1 && result.value.indexOf(step.toString()) !== -1)) {
done()
} else if (incr > 1000) {

@ -16,8 +16,8 @@ class OpenFile extends EventEmitter {
// click on fileExplorer can toggle it. We go through settings to be sure FE is open
function openFile (browser: NightwatchBrowser, name: string, done: VoidFunction) {
browser.clickLaunchIcon('settings').clickLaunchIcon('fileExplorers')
.waitForElementVisible('li[key="' + name + '"]')
.click('li[key="' + name + '"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItem' + name + '"')
.click('li[data-id="treeViewLitreeViewItem' + name + '"')
.pause(2000)
.perform(() => {
done()

@ -34,13 +34,19 @@ function removeFile (browser: NightwatchBrowser, path: string, done: VoidFunctio
contextMenuClick(document.querySelector('[data-path="' + path + '"]'))
}, [path], function () {
browser
.waitForElementVisible('#menuitemdelete', 2000)
.waitForElementVisible('#menuitemdelete')
.click('#menuitemdelete')
.pause(500)
.waitForElementVisible('#modal-footer-ok', 2000)
.click('#modal-footer-ok')
.waitForElementNotPresent('[data-path="' + path + '"]')
.pause(2000)
.perform(() => {
if (path.indexOf('browser') !== -1) {
browser.waitForElementVisible('[data-id="browser-modal-footer-ok-react"]')
.click('[data-id="browser-modal-footer-ok-react"]')
.waitForElementNotPresent('[data-path="' + path + '"]')
} else if (path.indexOf('localhost') !== -1) {
browser.waitForElementVisible('[data-id="localhost-modal-footer-ok-react"]')
.click('[data-id="localhost-modal-footer-ok-react"]')
.waitForElementNotPresent('[data-path="' + path + '"]')
}
done()
})
})

@ -1,10 +1,10 @@
import EventEmitter from 'events'
import { NightwatchBrowser } from 'nightwatch'
class RenameFile extends EventEmitter {
class RenamePath extends EventEmitter {
command (this: NightwatchBrowser, path: string, newFileName: string, renamedPath: string) {
this.api.perform((done) => {
renameFile(this.api, path, newFileName, renamedPath, () => {
renamePath(this.api, path, newFileName, renamedPath, () => {
done()
this.emit('complete')
})
@ -13,7 +13,7 @@ class RenameFile extends EventEmitter {
}
}
function renameFile (browser: NightwatchBrowser, path: string, newFileName: string, renamedPath: string, done: VoidFunction) {
function renamePath (browser: NightwatchBrowser, path: string, newFileName: string, renamedPath: string, done: VoidFunction) {
browser.execute(function (path: string) {
function contextMenuClick (element) {
const evt = element.ownerDocument.createEvent('MouseEvents')
@ -41,10 +41,9 @@ function renameFile (browser: NightwatchBrowser, path: string, newFileName: stri
doneSetValue()
})
})
.click('body') // blur
.waitForElementVisible('#modal-footer-ok', 100000)
.pause(1000)
.click('li[data-id="treeViewLitreeViewItembrowser/README.txt"]') // focus on root directory
.pause(2000)
.click('#modal-footer-ok')
.waitForElementNotPresent('[data-path="' + path + '"]')
.waitForElementPresent('[data-path="' + renamedPath + '"]')
.perform(() => {
@ -53,4 +52,4 @@ function renameFile (browser: NightwatchBrowser, path: string, newFileName: stri
})
}
module.exports = RenameFile
module.exports = RenamePath

@ -39,10 +39,11 @@ module.exports = {
'Debug Ballot / delegate': function (browser: NightwatchBrowser) {
browser.pause(500)
.click('*[data-id="txLoggerDebugButton0x41fab8ea5b1d9fba5e0a6545ca1a2d62fff518578802c033c2b9a031a01c31b3"]')
.pause(2000)
.waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
// .clickLaunchIcon('debugger')
.click('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.pause(2000)
.waitForElementVisible('#stepdetail')
.goToVMTraceStep(79)
.pause(1000)
.checkVariableDebug('soliditystate', stateCheck)

@ -50,8 +50,10 @@ module.exports = {
browser.pause(500)
.click('*[data-id="txLoggerDebugButton0x41fab8ea5b1d9fba5e0a6545ca1a2d62fff518578802c033c2b9a031a01c31b3"]')
.pause(2000)
.waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.click('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.pause(2000)
.waitForElementVisible('#stepdetail')
.goToVMTraceStep(20)
.pause(1000)
.checkVariableDebug('callstackpanel', ["0x692a70D2e424a56D2C6C27aA97D1a86395877b3A"])

@ -89,6 +89,7 @@ module.exports = {
.createContract('"tokenName", "symbol"')
.debugTransaction(2)
.pause(2000)
.waitForElementVisible('#stepdetail')
.goToVMTraceStep(10)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf(`constructor (string memory name_, string memory symbol_) public {
@ -109,7 +110,8 @@ module.exports = {
browser
.clickLaunchIcon('solidity')
.setSolidityCompilerVersion('soljson-v0.6.12+commit.27d51765.js')
.clickLaunchIcon('udapp')
.clickLaunchIcon('fileExplorers')
.click('li[data-id="treeViewLitreeViewItembrowser/externalImport.sol"')
.testContracts('withABIEncoderV2.sol', sources[2]['browser/withABIEncoderV2.sol'], ['test'])
.clickLaunchIcon('udapp')
.selectContract('test')
@ -117,7 +119,8 @@ module.exports = {
.clickInstance(2)
.clickFunction('test1 - transact (not payable)', {types: 'bytes userData', values: '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000015b38da6a701c568545dcfcb03fcb875f56beddc4'})
.debugTransaction(4)
.pause(2000)
.pause(2000)
.waitForElementVisible('#stepdetail')
.goToVMTraceStep(261)
.pause(1000)
/*

@ -20,10 +20,10 @@ module.exports = {
browser.waitForElementVisible('div[data-id="remixIdeSidePanel"]')
.assert.containsText('h6[data-id="sidePanelSwapitTitle"]', 'FILE EXPLORERS')
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.waitForElementVisible('li[key="browser/contracts"]')
.waitForElementVisible('li[key="browser/scripts"]')
.waitForElementVisible('li[key="browser/tests"]')
.waitForElementVisible('li[key="browser/README.txt"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/scripts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/tests"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/README.txt"]')
},
'Loads Main View': function (browser: NightwatchBrowser) {
@ -61,17 +61,17 @@ module.exports = {
'Toggles File Explorer Browser': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.assert.visible('ul[key="browser"]')
.click('div[data-id="treeViewTogglebrowser"]')
.assert.hidden('ul[key="browser"]')
.click('div[data-id="treeViewTogglebrowser"]')
.assert.visible('ul[key="browser"]')
.waitForElementPresent('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.click('[data-path="browser"]')
.waitForElementNotPresent('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.click('[data-path="browser"]')
.waitForElementPresent('[data-id="treeViewLitreeViewItembrowser/contracts"]')
},
'Switch Tabs using tabs icon': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.click('*[data-id="treeViewTogglebrowser/contracts"]')
.click('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.openFile('browser/contracts/3_Ballot.sol')
.assert.containsText('div[title="browser/contracts/3_Ballot.sol"]', '3_Ballot.sol')
.click('span[class^=dropdownCaret]')

@ -14,7 +14,6 @@ module.exports = {
browser.waitForElementVisible('div[data-id="mainPanelPluginsContainer"]')
.clickLaunchIcon('fileExplorers')
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.click('*[data-id="treeViewLibrowser/contracts"]')
.openFile('browser/contracts/1_Storage.sol')
.waitForElementVisible('*[data-id="editorInput"]')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '12px')
@ -81,7 +80,8 @@ module.exports = {
'Should highlight source code': function (browser: NightwatchBrowser) {
// include all files here because switching between plugins in side-panel removes highlight
browser.addFile('sourcehighlight.js', sourcehighlightScript)
browser
.addFile('sourcehighlight.js', sourcehighlightScript)
.addFile('removeSourcehighlightScript.js', removeSourcehighlightScript)
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript)
.openFile('browser/sourcehighlight.js')
@ -96,27 +96,26 @@ module.exports = {
},
'Should remove 1 highlight from source code': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[key="browser/removeSourcehighlightScript.js"]')
.click('li[key="browser/removeSourcehighlightScript.js"]')
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItembrowser/removeSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItembrowser/removeSourcehighlightScript.js"]')
.pause(2000)
.executeScript('remix.exeCurrent()')
.waitForElementVisible('li[key="browser/contracts"]')
.click('li[key="browser/contracts"]') // files don't appear, so we click twice to get the files
.click('li[key="browser/contracts"]')
.waitForElementVisible('li[key="browser/contracts/3_Ballot.sol"]')
.click('li[key="browser/contracts/3_Ballot.sol"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItembrowser/contracts"]')
.click('li[data-id="treeViewLitreeViewItembrowser/contracts"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItembrowser/contracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItembrowser/contracts/3_Ballot.sol"]')
.waitForElementNotPresent('.highlightLine32')
.checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)')
.checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)')
},
'Should remove all highlights from source code': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[key="browser/removeAllSourcehighlightScript.js"]')
.click('li[key="browser/removeAllSourcehighlightScript.js"]')
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItembrowser/removeAllSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItembrowser/removeAllSourcehighlightScript.js"]')
.pause(2000)
.executeScript('remix.exeCurrent()')
.waitForElementVisible('li[key="browser/contracts/3_Ballot.sol"]')
.click('li[key="browser/contracts/3_Ballot.sol"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItembrowser/contracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItembrowser/contracts/3_Ballot.sol"]')
.pause(2000)
.waitForElementNotPresent('.highlightLine32')
.waitForElementNotPresent('.highlightLine40')

@ -21,70 +21,74 @@ module.exports = {
.clickLaunchIcon('fileExplorers')
.assert.containsText('h6[data-id="sidePanelSwapitTitle"]', 'FILE EXPLORERS')
.click('*[data-id="fileExplorerNewFilecreateNewFile"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', '5_New_contract.sol')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="treeViewLibrowser/5_New_contract.sol"]', 7000)
.pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', '5_New_contract.sol')
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/5_New_contract.sol"]', 7000)
},
'Should rename `5_New_contract.sol` to 5_Renamed_Contract.sol': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="treeViewLibrowser/5_New_contract.sol"]')
.renameFile('browser/5_New_contract.sol', '5_Renamed_Contract.sol', 'browser/5_Renamed_Contract.sol')
.waitForElementVisible('*[data-id="treeViewLibrowser/5_Renamed_Contract.sol"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/5_New_contract.sol"]')
.renamePath('browser/5_New_contract.sol', '5_Renamed_Contract.sol', 'browser/5_Renamed_Contract.sol')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/5_Renamed_Contract.sol"]')
},
'Should delete file `5_Renamed_Contract.sol` from file explorer': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="treeViewLibrowser/5_Renamed_Contract.sol"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/5_Renamed_Contract.sol"]')
.rightClick('[data-path="browser/5_Renamed_Contract.sol"]')
.click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.modalFooterOKClick()
.waitForElementNotPresent('*[data-id="treeViewLibrowser/5_Renamed_Contract.sol"')
.waitForElementVisible('*[data-id="browserModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItembrowser/5_Renamed_Contract.sol"')
},
'Should create a new folder': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="treeViewLibrowser/README.txt"]')
.rightClick('[data-path="browser/README.txt"]')
.click('*[id="menuitemcreate folder"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', 'Browser_Tests')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="treeViewLibrowser/Browser_Tests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/README.txt"]')
.click('[data-id="fileExplorerNewFilecreateNewFolder"]')
.pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', 'Browser_Tests')
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/Browser_Tests"]')
},
'Should rename Browser_Tests folder to Browser_E2E_Tests': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="treeViewLibrowser/Browser_Tests"]')
.rightClick('[data-path="browser/Browser_Tests"]')
.click('*[id="menuitemrename"]')
.sendKeys('[data-path="browser/Browser_Tests"]', 'Browser_E2E_Tests')
.sendKeys('[data-path="browser/Browser_Tests"]', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLibrowser/Browser_E2E_Tests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/Browser_Tests"]')
.renamePath('browser/Browser_Tests', 'Browser_E2E_Tests', 'browser/Browser_E2E_Tests')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/Browser_E2E_Tests"]')
},
'Should delete Browser_E2E_Tests folder': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="treeViewLibrowser/Browser_E2E_Tests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/Browser_E2E_Tests"]')
.rightClick('[data-path="browser/Browser_E2E_Tests"]')
.click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.modalFooterOKClick()
.waitForElementNotPresent('*[data-id="treeViewLibrowser/Browser_E2E_Tests"]')
.waitForElementVisible('*[data-id="browserModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItembrowser/Browser_E2E_Tests"]')
},
'Should publish all explorer files to github gist': function (browser: NightwatchBrowser) {
const runtimeBrowser = browser.options.desiredCapabilities.browserName
browser
browser.refresh()
.pause(10000)
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="modalDialogContainer"]', 7000)
.modalFooterOKClick()
.waitForElementVisible('*[data-id="browserModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.pause(2000)
.waitForElementVisible('*[data-id="browserModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.pause(2000)
.perform((done) => {
if (runtimeBrowser === 'chrome') {
@ -101,9 +105,9 @@ module.exports = {
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3)
.waitForElementVisible('*[key="browser/editor.test.js"]')
.waitForElementVisible('*[key="browser/fileExplorer.test.js"]')
.waitForElementVisible('*[key="browser/generalSettings.test.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/editor.test.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/fileExplorer.test.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/generalSettings.test.js"]')
.end()
},

@ -63,7 +63,7 @@ module.exports = {
.addFile('renameFile.js', { content: executeRename })
.executeScript(`remix.exeCurrent()`)
.pause(2000)
.waitForElementPresent('[data-id="treeViewLibrowser/old_contract.sol"]')
.waitForElementPresent('[data-id="treeViewLitreeViewItembrowser/old_contract.sol"]')
},
'Should execute `mkdir` api from file manager external api': function (browser: NightwatchBrowser) {
@ -71,7 +71,7 @@ module.exports = {
.addFile('mkdirFile.js', { content: executeMkdir })
.executeScript(`remix.exeCurrent()`)
.pause(2000)
.waitForElementPresent('[data-id="treeViewLibrowser/Test_Folder"]')
.waitForElementPresent('[data-id="treeViewLitreeViewItembrowser/Test_Folder"]')
},
'Should execute `readdir` api from file manager external api': function (browser: NightwatchBrowser) {
@ -87,15 +87,16 @@ module.exports = {
.addFile('removeFile.js', { content: executeRemove })
.executeScript(`remix.exeCurrent()`)
.pause(2000)
.waitForElementNotPresent('[data-id="treeViewLibrowser/old_contract.sol"]')
.waitForElementNotPresent('[data-id="treeViewLitreeViewItembrowser/old_contract.sol"]')
},
'Should execute `remove` api from file manager external api on a folder': function (browser: NightwatchBrowser) {
// TODO: Fix remove root directory prefix for browser and localhost
'Should execute `remove` api from file manager external api on a folder': '' + function (browser: NightwatchBrowser) {
browser
.addFile('test_jsRemoveFolder.js', { content: executeRemoveOnFolder })
.executeScript('remix.exeCurrent()')
.pause(2000)
.waitForElementNotPresent('*[key="browser/tests"]')
.waitForElementNotPresent('[data-id="treeViewLitreeViewItembrowser/tests"]')
.end()
},

@ -20,7 +20,7 @@ module.exports = {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000)
.waitForElementVisible('*[data-id="settingsTabGenerateContractMetadataLabel"]', 5000)
.click('*[data-id="verticalIconsFileExplorerIcons"]')
.click('*[data-id="treeViewTogglebrowser/contracts"]')
.click('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.openFile('browser/contracts/3_Ballot.sol')
.click('*[data-id="verticalIconsKindsolidity"]')
.pause(2000)

@ -13,7 +13,7 @@ module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
'UploadToGists': function (browser: NightwatchBrowser) {
UploadToGists: function (browser: NightwatchBrowser) {
/*
- set the access token
- publish to gist
@ -24,18 +24,24 @@ module.exports = {
const runtimeBrowser = browser.options.desiredCapabilities.browserName
browser
.refresh()
.pause(10000)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('fileExplorers')
.rightClick('[data-path="browser/README.txt"]')
.click('*[id="menuitemcreate folder"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', 'Browser_Tests')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="treeViewLibrowser/Browser_Tests"]')
.click('[data-id="fileExplorerNewFilecreateNewFolder"]')
.pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', 'Browser_Tests')
.sendKeys('*[data-id="treeViewLitreeViewItembrowser/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItembrowser/Browser_Tests"]')
.addFile('File.sol', { content: '' })
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.modalFooterOKClick()
.getModalBody((value, done) => {
.waitForElementVisible('*[data-id="browserModalDialogContainer-react"]')
.pause(2000)
.click('.modal-ok')
.pause(10000)
.getText('[data-id="browserModalDialogModalBody-react"]', (result) => {
console.log(result)
const value = typeof result.value === 'string' ? result.value : null
const reg = /gist.github.com\/([^.]+)/
const id = value.match(reg)
@ -45,13 +51,12 @@ module.exports = {
} else {
const gistid = id[1]
browser
.modalFooterCancelClick()
.click('[data-id="browser-modal-footer-cancel-react"]')
.executeScript(`remix.loadgist('${gistid}')`)
.perform((done) => { if (runtimeBrowser === 'chrome') { browser.openFile('browser/gists') } done() })
.waitForElementVisible(`li[key="browser/gists/${gistid}"]`)
.click(`li[key="browser/gists/${gistid}"]`)
.waitForElementVisible(`[data-id="treeViewLitreeViewItembrowser/gists/${gistid}"]`)
.click(`[data-id="treeViewLitreeViewItembrowser/gists/${gistid}"]`)
.openFile(`browser/gists/${gistid}/README.txt`)
.perform(done)
}
})
},

@ -16,10 +16,11 @@ module.exports = {
browser
.waitForElementVisible('#icon-panel', 10000)
.clickLaunchIcon('fileExplorers')
.click('*[data-id="treeViewTogglebrowser/contracts"]')
.click('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.openFile('browser/contracts/3_Ballot.sol')
.verifyContracts(['Ballot'])
.click('#publishOnIpfs')
.pause(8000)
.getModalBody((value, done) => {
if (value.indexOf('Metadata of "ballot" was published successfully.') === -1) browser.assert.fail('ipfs deploy failed', '', '')
if (value.indexOf('dweb:/ipfs') === -1) browser.assert.fail('ipfs deploy failed', '', '')
@ -31,6 +32,7 @@ module.exports = {
'Publish on Swarm': '' + function (browser: NightwatchBrowser) {
browser
.click('#publishOnSwarm')
.pause(8000)
.getModalBody((value, done) => {
if (value.indexOf('Metadata of "ballot" was successfully.') === -1) browser.assert.fail('swarm deploy failed', '', '')
if (value.indexOf('bzz') === -1) browser.assert.fail('swarm deploy failed', '', '')
@ -43,24 +45,24 @@ module.exports = {
browser
.waitForElementVisible('#icon-panel')
.clickLaunchIcon('fileExplorers')
.click('*[data-id="treeViewLibrowser/contracts"]')
.click('*[data-id="treeViewLibrowser/contracts"]')
.openFile('browser/contracts/1_Storage.sol')
.clickLaunchIcon('udapp')
.waitForElementPresent('*[data-id="contractDropdownIpfsCheckbox"]')
.click('*[data-id="contractDropdownIpfsCheckbox"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.pause(5000)
.assert.containsText('*[data-id="modalDialogModalBody"]', 'Metadata of "storage" was published successfully.')
.pause(8000)
.getModalBody((value, done) => {
if (value.indexOf('Metadata of "storage" was published successfully.') === -1) browser.assert.fail('ipfs deploy failed', '', '')
done()
})
.modalFooterOKClick()
},
'Should remember choice after page refresh': function (browser: NightwatchBrowser) {
browser
.refresh()
.waitForElementVisible('*[data-id="treeViewLibrowser/contracts"]')
.click('*[data-id="treeViewLibrowser/contracts"]')
.click('*[data-id="treeViewLibrowser/contracts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.click('[data-id="treeViewLitreeViewItembrowser/contracts"]')
.openFile('browser/contracts/1_Storage.sol')
.clickLaunchIcon('udapp')
.waitForElementPresent('*[data-id="contractDropdownIpfsCheckbox"]')

@ -121,7 +121,7 @@ function runTests (browser: NightwatchBrowser) {
.setEditorValue('contract test1 { function get () returns (uint) { return 10; }}')
.click('[data-path="localhost/folder1/contract_' + browserName + '.sol"]') // rename a file and check
.pause(1000)
.renameFile('localhost/folder1/contract_' + browserName + '.sol', 'renamed_contract_' + browserName + '.sol', 'localhost/folder1/renamed_contract_' + browserName + '.sol')
.renamePath('localhost/folder1/contract_' + browserName + '.sol', 'renamed_contract_' + browserName + '.sol', 'localhost/folder1/renamed_contract_' + browserName + '.sol')
.pause(1000)
.removeFile('localhost/folder1/contract_' + browserName + '_toremove.sol')
.perform(function (done) {

@ -128,6 +128,7 @@ module.exports = {
.waitForElementPresent('.transaction-status--submitted')
.pause(25000)
.switchBrowserTab(0)
.end()
},
'Should connect to Ethereum Main Network using MetaMask': '' + function (browser: NightwatchBrowser) {

@ -48,6 +48,8 @@ module.exports = {
'Test Github Import - no branch specified': function (browser: NightwatchBrowser) {
browser
.setSolidityCompilerVersion('soljson-v0.6.2+commit.bacdbe57.js') // open-zeppelin moved to pragma ^0.6.0 (master branch)
.clickLaunchIcon('fileExplorers')
.click('li[data-id="treeViewLitreeViewItembrowser/README.txt"')
.addFile('Untitled6.sol', sources[5]['browser/Untitled6.sol'])
.clickLaunchIcon('fileExplorers')
.verifyContracts(['test10', 'ERC20', 'SafeMath'], {wait: 10000})
@ -55,6 +57,8 @@ module.exports = {
'Test Github Import - raw URL': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('fileExplorers')
.click('li[data-id="treeViewLitreeViewItembrowser/README.txt"')
.addFile('Untitled7.sol', sources[6]['browser/Untitled7.sol'])
.clickLaunchIcon('fileExplorers')
.verifyContracts(['test11', 'ERC20', 'SafeMath'], {wait: 10000})
@ -63,6 +67,8 @@ module.exports = {
'Test switch to a github import from a solidity warning': function (browser: NightwatchBrowser) {
browser
.setSolidityCompilerVersion('soljson-v0.7.4+commit.3f05b770.js')
.clickLaunchIcon('fileExplorers')
.click('li[data-id="treeViewLitreeViewItembrowser/README.txt"')
.addFile('Untitled8.sol', sources[7]['browser/Untitled8.sol'])
.clickLaunchIcon('fileExplorers')
.clickLaunchIcon('solidity')

@ -144,7 +144,8 @@ module.exports = {
},
'Changing current path': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('*[data-id="verticalIconsKindfileExplorers"]')
browser
.waitForElementPresent('*[data-id="verticalIconsKindfileExplorers"]')
.addFile('myTests/simple_storage_test.sol', sources[0]['browser/tests/simple_storage_test.sol'])
.clickLaunchIcon('solidityUnitTesting')
.setValue('*[data-id="uiPathInput"]', 'browser/myTests')
@ -167,7 +168,7 @@ function runTests (browser: NightwatchBrowser) {
browser
.waitForElementPresent('*[data-id="verticalIconsKindfileExplorers"]')
.clickLaunchIcon('fileExplorers')
.click('*[data-id="treeViewTogglebrowser/contracts"]')
.click('*[data-id="treeViewLitreeViewItembrowser/contracts"]')
.openFile('browser/contracts/3_Ballot.sol')
.clickLaunchIcon('solidityUnitTesting')
.pause(500)

@ -28,7 +28,7 @@ module.exports = {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('fileExplorers')
.addFile('browser/basic.sol', sources[0]['browser/basic.sol'])
.addFile('basic.sol', sources[0]['browser/basic.sol'])
.clickLaunchIcon('solidity')
.execute(function() {
const elem = document.getElementById('nightlies') as HTMLInputElement

@ -0,0 +1,35 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
import sauce from './sauce'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', false)
},
'Checks vertical icons panelcontex menu': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('*[data-id="verticalIconsKindpluginManager"]')
.click('*[data-id="verticalIconsKindpluginManager"]')
.scrollAndClick('*[data-id="pluginManagerComponentActivateButtondebugger"]')
.waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtondebugger"]', 7000)
.rightClick('[data-id="verticalIconsKinddebugger"]')
.waitForElementVisible('*[id="menuitemdeactivate"]')
.waitForElementVisible('*[id="menuitemdocumentation"]')
.click('*[data-id="remixIdeIconPanel"]')
},
'Checks vertical icons panel contex menu deactivate': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('*[data-id="verticalIconsKinddebugger"]', 7000)
.rightClick('[data-id="verticalIconsKinddebugger"]')
.click('*[id="menuitemdeactivate"]')
.click('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindpluginManager"]')
.scrollInto('*[data-id="pluginManagerComponentActivateButtondebugger"]')
.waitForElementVisible('*[data-id="pluginManagerComponentActivateButtondebugger"]')
},
tearDown: sauce
}

@ -28,7 +28,7 @@ declare module "nightwatch" {
checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser,
openFile(name: string): NightwatchBrowser,
editorScroll(direction: 'up' | 'down', numberOfTimes: number): NightwatchBrowser,
renameFile(path: string, newFileName: string, renamedPath: string): NightwatchBrowser,
renamePath(path: string, newFileName: string, renamedPath: string): NightwatchBrowser,
rightClick(cssSelector: string): NightwatchBrowser,
waitForElementContainsText(id: string, value: string): NightwatchBrowser,
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser,
@ -63,6 +63,10 @@ declare module "nightwatch" {
sendKeys: (selector: string, inputValue: string | string[], callback?: (this: NightwatchAPI, result: NightwatchCallbackResult<void>) => void) => NightwatchBrowser
}
export interface NightwatchAPI {
keys(keysToSend: string, callback?: (this: NightwatchAPI, result: NightwatchCallbackResult<void>) => void): NightwatchAPI
}
export interface NightwatchContractContent {
content: string;
}

@ -50,7 +50,7 @@ const CompilersArtefacts = require('./app/compiler/compiler-artefacts')
const CompileTab = require('./app/tabs/compile-tab')
const SettingsTab = require('./app/tabs/settings-tab')
const AnalysisTab = require('./app/tabs/analysis-tab')
const DebuggerTab = require('./app/tabs/debugger-tab')
const { DebuggerTab } = require('./app/tabs/debugger-tab')
const TestTab = require('./app/tabs/test-tab')
const FilePanel = require('./app/panels/file-panel')
const Editor = require('./app/editor/editor')
@ -370,10 +370,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
registry.get('fileproviders/browser').api
)
const analysis = new AnalysisTab(registry)
const debug = new DebuggerTab(
blockchain,
registry.get('editor').api,
registry.get('offsettolinecolumnconverter').api)
const debug = new DebuggerTab()
const test = new TestTab(
registry.get('filemanager').api,
registry.get('offsettolinecolumnconverter').api,

@ -26,10 +26,11 @@ export default class FetchAndCompile extends Plugin {
* Returns compilation data
*
* @param {string} contractAddress - Address of the contrac to resolve
* @param {string} compilersartefacts - Object containing a mapping of compilation results (byContractAddress and __last)
* @param {string} deployedBytecode - deployedBytecode of the contract
* @param {string} targetPath - Folder where to save the compilation arfefacts
* @return {CompilerAbstract} - compilation data targeting the given @arg contractAddress
*/
async resolve (contractAddress, targetPath, web3) {
async resolve (contractAddress, codeAtAddress, targetPath) {
contractAddress = ethutil.toChecksumAddress(contractAddress)
const compilersartefacts = globalRegistry.get('compilersartefacts').api
@ -52,7 +53,6 @@ export default class FetchAndCompile extends Plugin {
if (!this.sourceVerifierNetWork.includes(network.name)) return localCompilation()
// check if the contract if part of the local compilation result
const codeAtAddress = await web3.eth.getCode(contractAddress)
const compilation = localCompilation()
if (compilation) {
let found = false

@ -67,11 +67,20 @@ module.exports = class LocalPlugin {
this.profile[key] = e.target.value
}
updateMethods ({ target }) {
if (target.value) {
try {
this.profile.methods = target.value.split(',')
} catch (e) {}
}
}
/** The form to create a local plugin */
form () {
const name = this.profile.name || ''
const url = this.profile.url || ''
const displayName = this.profile.displayName || ''
const methods = (this.profile.methods && this.profile.methods.join(',')) || ''
const radioSelection = (key, label, message) => {
return this.profile[key] === label
? yo`<div class="radio">
@ -94,6 +103,12 @@ module.exports = class LocalPlugin {
<label for="plugin-displayname">Display Name</label>
<input class="form-control" onchange="${e => this.updateDisplayName(e)}" value="${displayName}" id="plugin-displayname" data-id="localPluginDisplayName" placeholder="Name in the header">
</div>
<div class="form-group">
<label for="plugin-methods">Api (comma separated list of methods name)</label>
<input class="form-control" onchange="${e => this.updateMethods(e)}" value="${methods}" id="plugin-methods" data-id="localPluginMethods" placeholder="Name in the header">
</div>
<div class="form-group">
<label for="plugin-url">Url <small>(required)</small></label>
<input class="form-control" onchange="${e => this.updateUrl(e)}" value="${url}" id="plugin-url" data-id="localPluginUrl" placeholder="ex: https://localhost:8000">

@ -4,9 +4,10 @@ var yo = require('yo-yo')
var csjs = require('csjs-inject')
var helper = require('../../lib/helper')
const globalRegistry = require('../../global/registry')
const contextMenu = require('../ui/contextMenu')
const { Plugin } = require('@remixproject/engine')
const EventEmitter = require('events')
let VERTICALMENU_HANDLE
const profile = {
name: 'menuicons',
@ -63,7 +64,7 @@ export class VerticalIcons extends Plugin {
* Add an icon to the map
* @param {ModuleProfile} profile The profile of the module
*/
addIcon ({ kind, name, icon, displayName, tooltip }) {
addIcon ({ kind, name, icon, displayName, tooltip, documentation }) {
let title = (tooltip || displayName || name)
title = title.replace(/^\w/, c => c.toUpperCase())
this.icons[name] = yo`
@ -72,6 +73,7 @@ export class VerticalIcons extends Plugin {
onclick="${() => { this.toggle(name) }}"
plugin="${name}"
title="${title}"
oncontextmenu="${(e) => this.itemContextMenu(e, name, documentation)}"
data-id="verticalIconsKind${name}">
<img class="image" src="${icon}" alt="${name}" />
</div>`
@ -221,6 +223,26 @@ export class VerticalIcons extends Plugin {
}
}
async itemContextMenu (e, name, documentation) {
const actions = {}
if (await this.appManager.canDeactivatePlugin(profile, { name })) {
actions.Deactivate = () => {
// this.call('manager', 'deactivatePlugin', name)
this.appManager.deactivatePlugin(name)
}
}
const links = {}
if (documentation) {
links.Documentation = documentation
}
if (Object.keys(actions).length || Object.keys(links).length) {
VERTICALMENU_HANDLE && VERTICALMENU_HANDLE.hide(null, true)
VERTICALMENU_HANDLE = contextMenu(e, actions, links)
}
e.preventDefault()
e.stopPropagation()
}
render () {
const home = yo`
<div

@ -174,29 +174,34 @@ class Editor extends Plugin {
const breakpoints = e.editor.session.getBreakpoints()
for (const k in breakpoints) {
if (k === row.toString()) {
this.event.trigger('breakpointCleared', [this.currentSession, row])
this.triggerEvent('breakpointCleared', [this.currentSession, row])
e.editor.session.clearBreakpoint(row)
e.stop()
return
}
}
this.setBreakpoint(row)
this.event.trigger('breakpointAdded', [this.currentSession, row])
this.triggerEvent('breakpointAdded', [this.currentSession, row])
e.stop()
})
// Do setup on initialisation here
this.editor.on('changeSession', () => {
this._onChange()
this.event.trigger('sessionSwitched', [])
this.triggerEvent('sessionSwitched', [])
this.editor.getSession().on('change', () => {
this._onChange()
this.sourceHighlighters.discardAllHighlights()
this.event.trigger('contentChanged', [])
this.triggerEvent('contentChanged', [])
})
})
}
triggerEvent (name, params) {
this.event.trigger(name, params) // internal stack
this.emit(name, ...params) // plugin stack
}
onActivation () {
this.on('sidePanel', 'focusChanged', (name) => this.sourceHighlighters.hideHighlightsExcept(name))
this.on('sidePanel', 'pluginDisabled', (name) => this.sourceHighlighters.discardHighlight(name))
@ -247,7 +252,7 @@ class Editor extends Plugin {
window.clearTimeout(this.saveTimeout)
}
this.saveTimeout = window.setTimeout(() => {
this.event.trigger('requiringToSaveCurrentfile', [])
this.triggerEvent('requiringToSaveCurrentfile', [])
}, 5000)
}

@ -424,15 +424,15 @@ class FileManager extends Plugin {
}
fileRemovedEvent (path) {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('fileRemoved', path)
this.events.emit('fileRemoved', path)
if (!this.openedFiles[path]) return
if (path === this._deps.config.get('currentFile')) {
this._deps.config.set('currentFile', '')
}
this.editor.discard(path)
delete this.openedFiles[path]
// TODO: Only keep `this.emit` (issue#2210)
this.emit('fileRemoved', path)
this.events.emit('fileRemoved', path)
this.openFile()
}

@ -115,6 +115,13 @@ module.exports = class RemixDProvider {
})
}
async createDir (path, cb) {
if (!this._isReady) return cb && cb('provider not ready')
const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'createDir', { path: unprefixedpath })
}
isReadOnly (path) {
return this._readOnlyMode || this._readOnlyFiles[path] === 1
}

@ -22,7 +22,7 @@ const profile = {
name: 'remixd',
displayName: 'RemixD',
url: 'ws://127.0.0.1:65520',
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'rename', 'remove', 'isDirectory', 'list'],
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'rename', 'remove', 'isDirectory', 'list', 'createDir'],
events: [],
description: 'Using Remixd daemon, allow to access file system',
kind: 'other',
@ -47,7 +47,6 @@ export class RemixdHandle extends WebsocketPlugin {
}
activate () {
this.fileSystemExplorer.show()
this.connectToLocalhost()
}
@ -83,7 +82,9 @@ export class RemixdHandle extends WebsocketPlugin {
this.canceled()
}
}, 3000)
this.locahostProvider.init(_ => this.fileSystemExplorer.ensureRoot())
this.locahostProvider.init(() => {
this.fileSystemExplorer.show()
})
this.call('manager', 'activatePlugin', 'git')
}
}

@ -1,13 +1,16 @@
import { ViewPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line
import './styles/file-panel-styles.css'
var yo = require('yo-yo')
var EventManager = require('../../lib/events')
var FileExplorer = require('../files/file-explorer')
// var FileExplorer = require('../files/file-explorer')
var { RemixdHandle } = require('../files/remixd-handle.js')
var { GitHandle } = require('../files/git-handle.js')
var globalRegistry = require('../../global/registry')
var css = require('./styles/file-panel-styles')
var canUpload = window.File || window.FileReader || window.FileList || window.Blob
@ -44,63 +47,85 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) {
super(profile)
var self = this
self._components = {}
self._components.registry = globalRegistry
self._deps = {
fileProviders: self._components.registry.get('fileproviders').api,
fileManager: self._components.registry.get('filemanager').api,
config: self._components.registry.get('config').api
this._components = {}
this._components.registry = globalRegistry
this._deps = {
fileProviders: this._components.registry.get('fileproviders').api,
fileManager: this._components.registry.get('filemanager').api,
config: this._components.registry.get('config').api
}
function createProvider (key, menuItems) {
return new FileExplorer(self._components.registry, self._deps.fileProviders[key], menuItems, self)
this.hideRemixdExplorer = true
this.remixdExplorer = {
hide: () => {
this.hideRemixdExplorer = true
this.renderComponent()
},
show: () => {
this.hideRemixdExplorer = false
this.renderComponent()
}
}
var fileExplorer = createProvider('browser', ['createNewFile', 'publishToGist', canUpload ? 'uploadFile' : ''])
var fileSystemExplorer = createProvider('localhost')
self.remixdHandle = new RemixdHandle(fileSystemExplorer, self._deps.fileProviders.localhost, appManager)
self.gitHandle = new GitHandle()
const explorers = yo`
<div>
<div class="pl-2 ${css.treeview}" data-id="filePanelFileExplorerTree">${fileExplorer.init()}</div>
<div class="pl-2 filesystemexplorer ${css.treeview}">${fileSystemExplorer.init()}</div>
this.el = yo`
<div id="fileExplorerView">
</div>
`
function template () {
return yo`
<div class=${css.container}>
<div class="${css.fileexplorer}">
<div class="${css.fileExplorerTree}">
${explorers}
</div>
</div>
</div>
`
}
this.remixdHandle = new RemixdHandle(this.remixdExplorer, this._deps.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle()
var event = new EventManager()
self.event = event
var element = template()
fileExplorer.ensureRoot()
self._deps.fileProviders.localhost.event.register('connecting', (event) => {
this.event = new EventManager()
this._deps.fileProviders.localhost.event.register('connecting', (event) => {
})
self._deps.fileProviders.localhost.event.register('connected', (event) => {
fileSystemExplorer.show()
this._deps.fileProviders.localhost.event.register('connected', (event) => {
this.remixdExplorer.show()
})
self._deps.fileProviders.localhost.event.register('errored', (event) => {
fileSystemExplorer.hide()
this._deps.fileProviders.localhost.event.register('errored', (event) => {
this.remixdExplorer.hide()
})
self._deps.fileProviders.localhost.event.register('closed', (event) => {
fileSystemExplorer.hide()
this._deps.fileProviders.localhost.event.register('closed', (event) => {
this.remixdExplorer.hide()
})
self.render = function render () { return element }
this.renderComponent()
}
render () {
return this.el
}
renderComponent () {
ReactDOM.render(
<div className='remixui_container'>
<div className='remixui_fileexplorer'>
<div className='remixui_fileExplorerTree'>
<div>
<div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'>
<FileExplorer
name='browser'
registry={this._components.registry}
filesProvider={this._deps.fileProviders.browser}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={this}
/>
</div>
<div className='pl-2 filesystemexplorer remixui_treeview'>
{ !this.hideRemixdExplorer &&
<FileExplorer
name='localhost'
registry={this._components.registry}
filesProvider={this._deps.fileProviders.localhost}
menuItems={['createNewFile', 'createNewFolder']}
plugin={this}
/>
}
</div>
</div>
</div>
</div>
</div>
, this.el)
}
}

@ -0,0 +1,56 @@
.remixui_container {
display : flex;
flex-direction : row;
width : 100%;
height : 100%;
box-sizing : border-box;
}
.remixui_fileexplorer {
display : flex;
flex-direction : column;
position : relative;
width : 100%;
padding-left : 6px;
padding-top : 6px;
}
.remixui_fileExplorerTree {
cursor : default;
}
.remixui_gist {
padding : 10px;
}
.remixui_gist i {
cursor : pointer;
}
.remixui_gist i:hover {
color : orange;
}
.remixui_connectToLocalhost {
padding : 10px;
}
.remixui_connectToLocalhost i {
cursor : pointer;
}
.remixui_connectToLocalhost i:hover {
color : var(--secondary)
}
.remixui_uploadFile {
padding : 10px;
}
.remixui_uploadFile label:hover {
color : var(--secondary)
}
.remixui_uploadFile label {
cursor : pointer;
}
.remixui_treeview {
overflow-y : auto;
}
.remixui_dialog {
display: flex;
flex-direction: column;
}
.remixui_dialogParagraph {
margin-bottom: 2em;
word-break: break-word;
}

@ -194,7 +194,7 @@ export class TabProxy extends Plugin {
}
this._view.filetabs.addTab({
id: name,
id: name.split(' ').join(''),
title,
icon,
tooltip: name

@ -1,7 +1,7 @@
import toaster from '../ui/tooltip'
import { DebuggerUI } from '@remix-ui/debugger-ui' // eslint-disable-line
import { DebuggerApiMixin } from '@remixproject/debugger-plugin'
import { ViewPlugin } from '@remixproject/engine-web'
import remixDebug, { TransactionDebugger as Debugger } from '@remix-project/remix-debug'
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
@ -21,16 +21,11 @@ const profile = {
version: packageJson.version
}
class DebuggerTab extends ViewPlugin {
constructor (blockchain, editor, offsetToLineColumnConverter) {
export class DebuggerTab extends DebuggerApiMixin(ViewPlugin) {
constructor () {
super(profile)
this.el = null
this.editor = editor
this.offsetToLineColumnConverter = offsetToLineColumnConverter
this.blockchain = blockchain
this.debugHash = null
this.removeHighlights = false
this.debugHashRequest = 0
this.initDebuggerApi()
}
render () {
@ -63,90 +58,12 @@ class DebuggerTab extends ViewPlugin {
this.renderComponent()
// this.call('manager', 'activatePlugin', 'udapp')
return this.el
}
async discardHighlight () {
await this.call('editor', 'discardHighlight')
}
async highlight (lineColumnPos, path) {
await this.call('editor', 'highlight', lineColumnPos, path)
}
async getFile (path) {
await this.call('fileManager', 'getFile', path)
}
async setFile (path, content) {
await this.call('fileManager', 'setFile', path, content)
}
renderComponent () {
ReactDOM.render(
<DebuggerUI debuggerAPI={this} />
, this.el)
}
deactivate () {
this.removeHighlights = true
this.renderComponent()
super.deactivate()
}
debug (hash) {
this.debugHash = hash
this.debugHashRequest++ // so we can trigger a debug using the same hash 2 times in a row. that's needs to be improved
this.renderComponent()
}
getDebugWeb3 () {
return new Promise((resolve, reject) => {
this.blockchain.detectNetwork((error, network) => {
let web3
if (error || !network) {
web3 = remixDebug.init.web3DebugNode(this.blockchain.web3())
} else {
const webDebugNode = remixDebug.init.web3DebugNode(network.name)
web3 = !webDebugNode ? this.blockchain.web3() : webDebugNode
}
remixDebug.init.extendWeb3(web3)
resolve(web3)
})
})
}
async getTrace (hash) {
if (!hash) return
const web3 = await this.getDebugWeb3()
const currentReceipt = await web3.eth.getTransactionReceipt(hash)
const debug = new Debugger({
web3,
offsetToLineColumnConverter: this.offsetToLineColumnConverter,
compilationResult: async (address) => {
try {
return await this.fetchContractAndCompile(address, currentReceipt)
} catch (e) {
console.error(e)
}
return null
},
debugWithGeneratedSources: false
})
return await debug.debugger.traceManager.getTrace(hash)
}
fetchContractAndCompile (address, receipt) {
const target = (address && remixDebug.traceHelper.isContractCreation(address)) ? receipt.contractAddress : address
const targetAddress = target || receipt.contractAddress || receipt.to
return this.call('fetchAndCompile', 'resolve', targetAddress, 'browser/.debug', this.blockchain.web3())
}
// debugger () {
// return this.debuggerUI
// }
}
module.exports = DebuggerTab

@ -52,7 +52,7 @@ module.exports = class TestTab extends ViewPlugin {
listenToEvents () {
this.filePanel.event.register('newTestFileCreated', file => {
var testList = this.view.querySelector("[class^='testList']")
var testList = this._view.el.querySelector("[class^='testList']")
var test = this.createSingleTest(file)
testList.appendChild(test)
this.data.allTests.push(file)

@ -30,7 +30,7 @@ var css = csjs`
}
`
module.exports = (event, items) => {
module.exports = (event, items, linkItems) => {
event.preventDefault()
function hide (event, force) {
@ -45,7 +45,21 @@ module.exports = (event, items) => {
current.onclick = () => { hide(null, true); items[item]() }
return current
})
const container = yo`<div id="menuItemsContainer" class="p-1 ${css.container} bg-light shadow border"><ul id='menuitems'>${menu}</ul></div>`
let menuForLinks = yo``
if (linkItems) {
menuForLinks = Object.keys(linkItems).map((item, index) => {
const current = yo`<li id="menuitem${item.toLowerCase()}" class=${css.liitem}><a href=${linkItems[item]} target="_blank">${item}</a></li>`
current.onclick = () => { hide(null, true) }
return current
})
}
const container = yo`
<div id="menuItemsContainer" class="p-1 ${css.container} bg-light shadow border">
<ul id='menuitems'>${menu} ${menuForLinks}</ul>
</div>
`
container.style.left = event.pageX + 'px'
container.style.top = event.pageY + 'px'

@ -6,10 +6,10 @@ module.exports = (title, content, ok, cancel, focusSelector, opts) => {
let agreed = true
let footerIsActive = false
opts = opts || {}
var container = document.querySelector('.modal')
var container = document.getElementById('modal-dialog')
if (!container) {
document.querySelector('body').appendChild(html(opts))
container = document.querySelector('.modal')
container = document.getElementById('modal-dialog')
incomingModal = false
} else incomingModal = true
@ -24,8 +24,8 @@ module.exports = (title, content, ok, cancel, focusSelector, opts) => {
cancelDiv.innerHTML = (cancel && cancel.label !== undefined) ? cancel.label : 'Cancel'
cancelDiv.style.display = cancelDiv.innerHTML === '' ? 'none' : 'inline-block'
var modal = document.querySelector('.modal-body')
var modalTitle = document.querySelector('.modal-header h6')
var modal = document.getElementById('modal-body-id')
var modalTitle = document.getElementById('modal-title-h6')
modalTitle.innerHTML = ''
if (title) modalTitle.innerText = title
@ -134,12 +134,12 @@ function html (opts) {
<div id="modal-background" class="modal-dialog" role="document">
<div class="modal-content ${css.modalContent} ${opts.class}">
<div class="modal-header">
<h6 class="modal-title" data-id="modalDialogModalTitle"></h6>
<h6 id="modal-title-h6" class="modal-title" data-id="modalDialogModalTitle"></h6>
<span class="modal-close">
<i id="modal-close" title="Close" class="fas fa-times" aria-hidden="true"></i>
</span>
</div>
<div class="modal-body ${css.modalBody}" data-id="modalDialogModalBody"> - </div>
<div id="modal-body-id" class="modal-body ${css.modalBody}" data-id="modalDialogModalBody"> - </div>
<div class="modal-footer" data-id="modalDialogModalFooter" autofocus>
<span id="modal-footer-ok" class="${css.modalFooterOk} modal-ok btn btn-sm btn-light" tabindex='5'>OK</span>
<span id="modal-footer-cancel" class="${css.modalFooterCancel} modal-cancel btn btn-sm btn-light" tabindex='10' data-dismiss="modal">Cancel</span>

@ -5,7 +5,7 @@ import { sourceMappingDecoder } from '@remix-project/remix-debug'
const profile = {
name: 'offsetToLineColumnConverter',
methods: [],
methods: ['offsetToLineColumn'],
events: [],
version: packageJson.version
}

@ -6,17 +6,29 @@ import QueryParams from './lib/query-params'
import { PermissionHandler } from './app/ui/persmission-handler'
const requiredModules = [ // services + layout views + system views
'manager', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'fileManager', 'contentImport', 'web3Provider', 'scriptRunner', 'fetchAndCompile',
'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', 'fileExplorers',
'terminal', 'settings', 'pluginManager', 'tabs']
'manager', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme',
'fileManager', 'contentImport', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons',
'fileExplorers', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp']
export function isNative (name) {
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd']
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons']
return nativePlugins.includes(name) || requiredModules.includes(name)
}
export function canActivate (name) {
return ['ethdoc'].includes(name) || isNative(name)
/**
* Checks if plugin caller 'from' is allowed to activate plugin 'to'
* The caller can have 'canActivate' as a optional property in the plugin profile.
* This is an array containing the 'name' property of the plugin it wants to call.
* canActivate = ['plugin1-to-call','plugin2-to-call',....]
* or the plugin is allowed by default because it is native
*
* @param {any, any}
* @returns {boolean}
*/
export function canActivate (from, to) {
return ['ethdoc'].includes(from.name) ||
isNative(from.name) ||
(to && from && from.canActivate && from.canActivate.includes[to.name])
}
export class RemixAppManager extends PluginManager {
@ -29,7 +41,7 @@ export class RemixAppManager extends PluginManager {
}
async canActivatePlugin (from, to) {
return canActivate(from.name)
return canActivate(from, to)
}
async canDeactivatePlugin (from, to) {

@ -37,7 +37,7 @@ export class Debugger {
locationToRowConverter: async (sourceLocation) => {
const compilationResult = await this.compilationResult()
if (!compilationResult) return { start: null, end: null }
return this.offsetToLineColumnConverter.offsetToLineColumn(sourceLocation, sourceLocation.file, compilationResult.source.sources, compilationResult.data.sources)
return await this.offsetToLineColumnConverter.offsetToLineColumn(sourceLocation, sourceLocation.file, compilationResult.source.sources, compilationResult.data.sources)
}
})
@ -70,7 +70,7 @@ export class Debugger {
const compilationResultForAddress = await this.compilationResult(address)
if (!compilationResultForAddress) return
this.debugger.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, index, compilationResultForAddress.data.contracts).then((rawLocation) => {
this.debugger.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, index, compilationResultForAddress.data.contracts).then(async (rawLocation) => {
if (compilationResultForAddress && compilationResultForAddress.data) {
const generatedSources = this.debugger.callTree.sourceLocationTracker.getGeneratedSourcesFromAddress(address)
const astSources = Object.assign({}, compilationResultForAddress.data.sources)
@ -81,7 +81,7 @@ export class Debugger {
sources[genSource.name] = { content: genSource.contents }
}
}
var lineColumnPos = this.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, sources, astSources)
var lineColumnPos = await this.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, sources, astSources)
this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation, generatedSources, address])
} else {
this.event.trigger('newSourceLocation', [null])

@ -26,7 +26,7 @@ export class EventManager {
obj = this.anonymous
}
for (const reg in this.registered[eventName]) {
if (this.registered[eventName][reg].obj === obj && this.registered[eventName][reg].func === func) {
if ((this.registered[eventName][reg].obj === obj) && (this.registered[eventName][reg].func.toString() === func.toString())) {
this.registered[eventName].splice(reg, 1)
}
}

@ -1 +1,3 @@
export * from './lib/debugger-ui'
export * from './lib/idebugger-api'
export * from './lib/idebugger-api'

@ -1,64 +0,0 @@
import type { CompilationResult, CompilationSource } from '@remix-project/remix-solidity-ts'
export interface DebuggerUIProps {
debuggerAPI: DebuggerAPI
}
interface EditorEvent {
event: {
register(eventName: 'breakpointCleared' | 'breakpointAdded' | 'contentChanged',
callback: (fileName: string, row: string | number) => void)
}
}
interface LineColumnLocation {
start: {
line: number, column: number
},
end: {
line: number, column: number
}
}
interface RawLocation {
start: number, length: number
}
interface Sources {
[fileName: string] : {content: string}
}
interface CompilationOutput {
source: { sources: Sources, target: string }
data: CompilationResult
getSourceName: (id: number) => string
}
interface Asts {
[fileName: string] : CompilationSource // ast
}
interface TransactionReceipt {
blockHash: string
blockNumber: number
transactionHash: string
transactionIndex: number
from: string
to: string
contractAddress: string | null
}
export interface DebuggerAPI {
offsetToLineColumnConverter: { offsetToLineColumn: (sourceLocation: RawLocation, file: number, contents: Sources, asts: Asts) => LineColumnLocation }
debugHash: string
debugHashRequest: string
removeHighlights: boolean
editor: EditorEvent
discardHighlight: () => void
highlight: (lineColumnPos: LineColumnLocation, path: string) => void
fetchContractAndCompile: (address: string, currentReceipt: TransactionReceipt) => CompilationOutput
getFile: (path: string) => string
setFile: (path: string, content: string) => void
getDebugWeb3: () => any // returns an instance of web3.js
}

@ -4,7 +4,7 @@ import StepManager from './step-manager/step-manager'
import VmDebugger from './vm-debugger/vm-debugger'
import VmDebuggerHead from './vm-debugger/vm-debugger-head'
import { TransactionDebugger as Debugger } from '@remix-project/remix-debug'
import { DebuggerUIProps } from './DebuggerAPI'
import { DebuggerUIProps } from './idebugger-api'
import { Toaster } from '@remix-ui/toaster'
/* eslint-disable-next-line */
import './debugger-ui.css'
@ -25,39 +25,35 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
opt: {
debugWithGeneratedSources: false
},
toastMessage: ''
toastMessage: '',
currentDebugTransaction: ''
})
useEffect(() => {
return unLoad()
}, [])
useEffect(() => {
if (debuggerModule.debugHash) {
debug(debuggerModule.debugHash)
}
}, [debuggerModule.debugHashRequest])
debuggerModule.onDebugRequested((hash) => {
if (hash) debug(hash)
})
useEffect(() => {
if (debuggerModule.removeHighlights) deleteHighlights()
}, [debuggerModule.removeHighlights])
debuggerModule.onRemoveHighlights(async () => {
await debuggerModule.discardHighlight()
})
useEffect(() => {
const setEditor = () => {
const editor = debuggerModule.editor
editor.event.register('breakpointCleared', (fileName, row) => {
debuggerModule.onBreakpointCleared((fileName, row) => {
if (state.debugger) state.debugger.breakPointManager.remove({fileName: fileName, row: row})
})
editor.event.register('breakpointAdded', (fileName, row) => {
if (state.debugger) {
state.debugger.breakPointManager.add({fileName: fileName, row: row})
}
debuggerModule.onBreakpointAdded((fileName, row) => {
if (state.debugger) state.debugger.breakPointManager.add({fileName: fileName, row: row})
})
editor.event.register('contentChanged', () => {
unLoad()
debuggerModule.onEditorContentChanged(() => {
if (state.debugger) unLoad()
})
}
@ -117,10 +113,6 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
unLoad()
}
const isDebuggerActive = () => {
return state.isActive
}
const unLoad = () => {
if (state.debugger) state.debugger.unload()
setState(prevState => {
@ -138,13 +130,20 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
vmDebugger: false,
vmDebuggerHead: false
},
debugging: false
debugging: false,
currentDebugTransaction: ''
}
})
}
const startDebugging = async (blockNumber, txNumber, tx) => {
if (state.debugger) unLoad()
if (!txNumber) return
setState(prevState => {
return {
...prevState,
currentDebugTransaction: txNumber
}
})
const web3 = await debuggerModule.getDebugWeb3()
const currentReceipt = await web3.eth.getTransactionReceipt(txNumber)
const debuggerInstance = new Debugger({
@ -189,12 +188,6 @@ const debug = (txHash) => {
startDebugging(null, txHash, null)
}
const deleteHighlights = async () => {
await debuggerModule.discardHighlight()
}
const stepManager = {
jumpTo: state.debugger && state.debugger.step_manager ? state.debugger.step_manager.jumpTo.bind(state.debugger.step_manager) : null,
stepOverBack: state.debugger && state.debugger.step_manager ? state.debugger.step_manager.stepOverBack.bind(state.debugger.step_manager) : null,

@ -0,0 +1,66 @@
import type { CompilationResult, CompilationSource } from '@remix-project/remix-solidity-ts'
export interface DebuggerUIProps {
debuggerAPI: IDebuggerApi
}
export interface LineColumnLocation {
start: {
line: number, column: number
},
end: {
line: number, column: number
}
}
export interface RawLocation {
start: number, length: number
}
export interface Sources {
[fileName: string] : {content: string}
}
export interface CompilationOutput {
source: { sources: Sources, target: string }
data: CompilationResult
getSourceName: (id: number) => string
}
export interface Asts {
[fileName: string] : CompilationSource // ast
}
export interface TransactionReceipt {
blockHash: string
blockNumber: number
transactionHash: string
transactionIndex: number
from: string
to: string
contractAddress: string | null
}
export type onBreakpointClearedListener = (params: string, row: number) => void
export type onBreakpointAddedListener = (params: string, row: number) => void
export type onEditorContentChanged = () => void
export type onDebugRequested = (hash: string) => void
export interface IDebuggerApi {
offsetToLineColumnConverter: { offsetToLineColumn: (sourceLocation: RawLocation, file: number, contents: Sources, asts: Asts) => Promise<LineColumnLocation> }
debugHash: string
debugHashRequest: number
removeHighlights: boolean
onRemoveHighlights: (listener: VoidFunction) => void
onDebugRequested: (listener: onDebugRequested) => void
onBreakpointCleared: (listener: onBreakpointClearedListener) => void
onBreakpointAdded: (listener: onBreakpointAddedListener) => void
onEditorContentChanged: (listener: onEditorContentChanged) => void
discardHighlight: () => Promise<void>
highlight: (lineColumnPos: LineColumnLocation, path: string) => Promise<void>
fetchContractAndCompile: (address: string, currentReceipt: TransactionReceipt) => Promise<CompilationOutput>
getFile: (path: string) => Promise<string>
setFile: (path: string, content: string) => Promise<void>
getDebugWeb3: () => any // returns an instance of web3.js
}

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

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

@ -0,0 +1,7 @@
# remix-ui-file-explorer
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remix-ui-file-explorer` to execute the unit tests via [Jest](https://jestjs.io).

@ -0,0 +1 @@
export * from './lib/file-explorer'

@ -0,0 +1,28 @@
.remixui_contextContainer
{
display: block;
position: fixed;
border-radius: 2px;
z-index: 1000;
box-shadow: 0 0 4px var(--dark);
}
.remixui_contextContainer:focus {
outline: none;
}
.remixui_liitem
{
padding: 2px;
padding-left: 6px;
cursor: pointer;
color: var(--text-dark);
background-color: var(--light);
}
.remixui_liitem:hover
{
background-color: var(--secondary);
}
#remixui_menuitems
{
list-style: none;
margin: 0px;
}

@ -0,0 +1,56 @@
.remixui_label {
margin-top : 4px;
}
.remixui_leaf {
overflow : hidden;
text-overflow : ellipsis;
width : 90%;
margin-bottom : 0px;
}
.remixui_fileexplorer {
box-sizing : border-box;
user-select : none;
}
input[type="file"] {
display: none;
}
.remixui_folder,
.remixui_file {
font-size : 14px;
cursor : pointer;
}
.remixui_file {
padding : 4px;
}
.remixui_newFile {
padding-right : 10px;
}
.remixui_newFile i {
cursor : pointer;
}
.remixui_newFile:hover {
transform : scale(1.3);
}
.remixui_menu {
margin-left : 20px;
}
.remixui_items {
display : inline
}
.remixui_remove {
margin-left : auto;
padding-left : 5px;
padding-right : 5px;
}
.remixui_activeMode {
display : flex;
width : 100%;
margin-right : 10px;
padding-right : 19px;
}
.remixui_activeMode > div {
min-width : 10px;
}
ul {
padding : 0;
}

@ -0,0 +1,81 @@
import React, { useRef, useEffect } from 'react' // eslint-disable-line
import { FileExplorerContextMenuProps } from './types'
import './css/file-explorer-context-menu.css'
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, publishToGist, runScript, pageX, pageY, path, type, ...otherProps } = props
const contextMenuRef = useRef(null)
useEffect(() => {
contextMenuRef.current.focus()
}, [])
useEffect(() => {
const menuItemsContainer = contextMenuRef.current
const boundary = menuItemsContainer.getBoundingClientRect()
if (boundary.bottom > (window.innerHeight || document.documentElement.clientHeight)) {
menuItemsContainer.style.position = 'absolute'
menuItemsContainer.style.bottom = '10px'
menuItemsContainer.style.top = null
}
}, [pageX, pageY])
const menu = () => {
return actions.filter(item => {
if (item.type.findIndex(name => name === type) !== -1) return true
else if (item.path.findIndex(key => key === path) !== -1) return true
else if (item.extension.findIndex(ext => path.endsWith(ext)) !== -1) return true
else if (item.pattern.filter(value => path.match(new RegExp(value))).length > 0) return true
else return false
}).map((item, index) => {
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
className='remixui_liitem'
onClick={(e) => {
e.stopPropagation()
switch (item.name) {
case 'New File':
createNewFile(path)
break
case 'New Folder':
createNewFolder(path)
break
case 'Rename':
renamePath(path, type)
break
case 'Delete':
deletePath(path)
break
case 'Push changes to gist':
publishToGist()
break
case 'Run':
runScript(path)
break
default:
break
}
hideContextMenu()
}}>{item.name}</li>
})
}
return (
<div
id="menuItemsContainer"
className="p-1 remixui_contextContainer bg-light shadow border"
style={{ left: pageX, top: pageY }}
ref={contextMenuRef}
onBlur={hideContextMenu}
tabIndex={500}
{...otherProps}
>
<ul id='remixui_menuitems'>{menu()}</ul>
</div>
)
}
export default FileExplorerContextMenu

@ -0,0 +1,97 @@
import React, { useState, useEffect } from 'react' //eslint-disable-line
import { FileExplorerMenuProps } from './types'
export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
const [state, setState] = useState({
menuItems: [
{
action: 'createNewFile',
title: 'Create New File',
icon: 'far fa-file'
},
{
action: 'createNewFolder',
title: 'Create New Folder',
icon: 'far fa-folder'
},
{
action: 'publishToGist',
title: 'Publish all [browser] explorer files to a github gist',
icon: 'fab fa-github'
},
{
action: 'uploadFile',
title: 'Load a local file into Remix\'s browser folder',
icon: 'fa fa-upload'
},
{
action: 'updateGist',
title: 'Update the current [gist] explorer',
icon: 'fab fa-github'
}
].filter(item => props.menuItems && props.menuItems.find((name) => { return name === item.action })),
actions: {}
})
useEffect(() => {
const actions = {
updateGist: () => {}
}
setState(prevState => {
return { ...prevState, actions }
})
}, [])
return (
<>
<span className='remixui_label' title={props.title} data-path={props.title} style={{ fontWeight: 'bold' }}>{ props.title }</span>
<span className="remixui_menu">{
state.menuItems.map(({ action, title, icon }, index) => {
if (action === 'uploadFile') {
return (
<label
id={action}
data-id={'fileExplorerUploadFile' + action }
className={icon + ' mb-0 remixui_newFile'}
title={title}
key={index}
>
<input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onChange={(e) => {
e.stopPropagation()
props.uploadFile(e.target)
}}
multiple />
</label>
)
} else {
return (
<span
id={action}
data-id={'fileExplorerNewFile' + action}
onClick={(e) => {
e.stopPropagation()
if (action === 'createNewFile') {
props.createNewFile()
} else if (action === 'createNewFolder') {
props.createNewFolder()
} else if (action === 'publishToGist') {
props.publishToGist()
} else {
state.actions[action]()
}
}}
className={'newFile ' + icon + ' remixui_newFile'}
title={title}
key={index}
>
</span>
)
}
})}
</span>
</>
)
}
export default FileExplorerMenu

@ -0,0 +1,947 @@
import React, { useEffect, useState, useRef } 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
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import * as async from 'async'
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 * as helper from '../../../../../apps/remix-ide/src/lib/helper'
import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
import './css/file-explorer.css'
const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => {
const { filesProvider, name, registry, plugin } = props
const [state, setState] = useState({
focusElement: [{
key: name,
type: 'folder'
}],
focusPath: null,
files: [],
fileManager: null,
accessToken: null,
ctrlKey: false,
newFileName: '',
actions: [],
focusContext: {
element: null,
x: null,
y: null
},
focusEdit: {
element: null,
type: '',
isNew: false,
lastEdit: ''
},
expandPath: [],
modalOptions: {
hide: true,
title: '',
message: '',
ok: {
label: 'Ok',
fn: null
},
cancel: {
label: 'Cancel',
fn: null
},
handleHide: null
},
toasterMsg: ''
})
const editRef = useRef(null)
useEffect(() => {
if (state.focusEdit.element) {
setTimeout(() => {
if (editRef && editRef.current) {
editRef.current.focus()
}
}, 150)
}
}, [state.focusEdit.element])
useEffect(() => {
(async () => {
const fileManager = registry.get('filemanager').api
const config = registry.get('config').api
const accessToken = config.get('settings/gist-access-token')
const files = await fetchDirectoryContent(name)
const actions = [{
name: 'New File',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
name: 'New Folder',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
name: 'Rename',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
name: 'Delete',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
name: 'Push changes to gist',
type: [],
path: [],
extension: [],
pattern: ['^browser/gists/([0-9]|[a-z])*$']
}, {
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: []
}]
setState(prevState => {
return { ...prevState, fileManager, accessToken, files, actions }
})
})()
}, [])
useEffect(() => {
if (state.fileManager) {
filesProvider.event.register('fileExternallyChanged', fileExternallyChanged)
filesProvider.event.register('fileRenamedError', fileRenamedError)
}
}, [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])
const resolveDirectory = async (folderPath, dir: File[], isChild = false): Promise<File[]> => {
if (!isChild && (state.focusEdit.element === 'browser/blank') && state.focusEdit.isNew && (dir.findIndex(({ path }) => path === 'browser/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<File[]> => {
return new Promise((resolve) => {
filesProvider.resolveDirectory(folderPath, (error, fileTree) => {
if (error) console.error(error)
const files = normalize(folderPath, fileTree)
resolve(files)
})
})
}
const normalize = (path, filesList): File[] => {
const folders = []
const files = []
const prefix = path.split('/')[0]
Object.keys(filesList || {}).forEach(key => {
const path = prefix + '/' + key
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('/')
return keyPath[keyPath.length - 1]
}
const extractParentFromKey = (key: string):string => {
if (!key) return
const keyPath = key.split('/')
keyPath.pop()
return keyPath.join('/')
}
const createNewFile = (newFilePath: string) => {
const fileManager = state.fileManager
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, '')
if (!createFile) {
toast('Failed to create file ' + newName)
}
}
})
}
const createNewFolder = async (newFolderPath: string) => {
const fileManager = state.fileManager
const dirName = newFolderPath + '/'
try {
const exists = await fileManager.exists(dirName)
if (exists) return
await fileManager.mkdir(dirName)
// addFolder(parentFolder, newFolderPath)
} catch (e) {
console.log('error: ', e)
toast('Failed to create folder: ' + newFolderPath)
}
}
const deletePath = async (path: string) => {
if (filesProvider.isReadOnly(path)) {
return toast('cannot delete file. ' + name + ' is a read only explorer')
}
const isDir = state.fileManager.isDirectory(path)
modal('Delete file', `Are you sure you want to delete ${path} ${isDir ? 'folder' : 'file'}?`, {
label: 'Ok',
fn: async () => {
try {
const fileManager = state.fileManager
await fileManager.remove(path)
} catch (e) {
toast(`Failed to remove file ${path}.`)
}
}
}, {
label: 'Cancel',
fn: () => {}
})
}
const renamePath = async (oldPath: string, newPath: string) => {
try {
const fileManager = state.fileManager
const exists = await fileManager.exists(newPath)
if (exists) {
modal('Rename File Failed', 'File name already exists', {
label: 'Close',
fn: () => {}
}, null)
} else {
await fileManager.rename(oldPath, newPath)
}
} catch (error) {
modal('Rename File Failed', 'Unexpected error while renaming: ' + error, {
label: 'Close',
fn: async () => {}
}, null)
}
}
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, focusElement: [{ key: filePath, type: 'file' }] }
})
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, focusElement: [{ key: folderPath, type: 'folder' }] }
})
}
const fileExternallyChanged = (path: string, file: { content: string }) => {
const config = registry.get('config').api
if (config.get('currentFile') === path && registry.editor.currentContent() && registry.editor.currentContent() !== file.content) {
if (filesProvider.isReadOnly(path)) return registry.editor.setText(file.content)
modal(path + ' changed', 'This file has been changed outside of Remix IDE.', {
label: 'Replace by the new content',
fn: () => {
registry.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)
}
const uploadFile = (target) => {
// 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
// pick that up via the 'fileAdded' event from the files module.
[...target.files].forEach((file) => {
const files = filesProvider
const loadFile = (name: string): void => {
const fileReader = new FileReader()
fileReader.onload = async function (event) {
if (helper.checkSpecialChars(file.name)) {
modal('File Upload Failed', 'Special characters are not allowed', {
label: 'Close',
fn: async () => {}
}, null)
return
}
const success = await files.set(name, event.target.result)
if (!success) {
modal('File Upload Failed', 'Failed to create file ' + name, {
label: 'Close',
fn: async () => {}
}, null)
}
}
fileReader.readAsText(file)
}
const name = files.type + '/' + file.name
files.exists(name, (error, exist) => {
if (error) console.log(error)
if (!exist) {
loadFile(name)
} else {
modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, {
label: 'Ok',
fn: () => {
loadFile(name)
}
}, {
label: 'Cancel',
fn: () => {}
})
}
})
})
}
const publishToGist = () => {
modal('Create a public gist', 'Are you sure you want to publish all your files in browser directory anonymously as a public gist on github.com? Note: this will not include directories.', {
label: 'Ok',
fn: toGist
}, {
label: 'Cancel',
fn: () => {}
})
}
const toGist = (id?: string) => {
const proccedResult = function (error, data) {
if (error) {
modal('Publish to gist Failed', 'Failed to manage gist: ' + error, {
label: 'Close',
fn: async () => {}
}, null)
} else {
if (data.html_url) {
modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, {
label: 'Ok',
fn: () => {
window.open(data.html_url, '_blank')
}
}, {
label: 'Cancel',
fn: () => {}
})
} else {
modal('Publish to gist Failed', data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t'), {
label: 'Close',
fn: async () => {}
}, null)
}
}
}
/**
* This function is to get the original content of given gist
* @params id is the gist id to fetch
*/
const getOriginalFiles = async (id) => {
if (!id) {
return []
}
const url = `https://api.github.com/gists/${id}`
const res = await fetch(url)
const data = await res.json()
return data.files || []
}
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
const folder = id ? 'browser/gists/' + id : 'browser/'
packageFiles(filesProvider, folder, async (error, packaged) => {
if (error) {
console.log(error)
modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, {
label: 'Close',
fn: async () => {}
}, null)
} else {
// check for token
if (!state.accessToken) {
modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', {
label: 'Close',
fn: async () => {}
}, null)
} else {
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist='
const gists = new Gists({ token: state.accessToken })
if (id) {
const originalFileList = await getOriginalFiles(id)
// Telling the GIST API to remove files
const updatedFileList = Object.keys(packaged)
const allItems = Object.keys(originalFileList)
.filter(fileName => updatedFileList.indexOf(fileName) === -1)
.reduce((acc, deleteFileName) => ({
...acc,
[deleteFileName]: null
}), originalFileList)
// adding new files
updatedFileList.forEach((file) => {
const _items = file.split('/')
const _fileName = _items[_items.length - 1]
allItems[_fileName] = packaged[file]
})
toast('Saving gist (' + id + ') ...')
gists.edit({
description: description,
public: true,
files: allItems,
id: id
}, (error, result) => {
proccedResult(error, result)
if (!error) {
for (const key in allItems) {
if (allItems[key] === null) delete allItems[key]
}
}
})
} else {
// id is not existing, need to create a new gist
toast('Creating a new gist ...')
gists.create({
description: description,
public: true,
files: packaged
}, (error, result) => {
proccedResult(error, result)
})
}
}
}
})
}
const runScript = async (path: string) => {
filesProvider.get(path, (error, content: string) => {
if (error) return console.log(error)
plugin.call('scriptRunner', 'execute', content)
})
}
const handleHideModal = () => {
setState(prevState => {
return { ...prevState, modalOptions: { ...state.modalOptions, hide: true } }
})
}
const modal = (title: string, message: string, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }) => {
setState(prevState => {
return {
...prevState,
modalOptions: {
...prevState.modalOptions,
hide: false,
message,
title,
ok,
cancel,
handleHide: handleHideModal
}
}
})
}
const toast = (message: string) => {
setState(prevState => {
return { ...prevState, toasterMsg: message }
})
}
const handleClickFile = (path: string) => {
state.fileManager.open(path)
setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'file' }] }
})
}
const handleClickFolder = async (path: string) => {
if (state.ctrlKey) {
if (state.focusElement.findIndex(item => item.key === path) !== -1) {
setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement.filter(item => item.key !== path)] }
})
} else {
setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement, { key: path, type: 'folder' }] }
})
}
} else {
let expandPath = []
if (!state.expandPath.includes(path)) {
expandPath = [...new Set([...state.expandPath, path])]
} else {
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))]
}
setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'folder' }], expandPath }
})
}
}
const handleContextMenuFile = (pageX: number, pageY: number, path: string, content: string) => {
if (!content) return
setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY }, focusEdit: { ...prevState.focusEdit, lastEdit: content } }
})
}
const handleContextMenuFolder = (pageX: number, pageY: number, path: string, content: string) => {
if (!content) return
setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY }, focusEdit: { ...prevState.focusEdit, lastEdit: content } }
})
}
const hideContextMenu = () => {
setState(prevState => {
return { ...prevState, focusContext: { element: null, x: 0, y: 0 } }
})
}
const editModeOn = (path: string, type: string, isNew: boolean = false) => {
if (filesProvider.isReadOnly(path)) return
setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
})
}
const editModeOff = async (content: string) => {
const parentFolder = extractParentFromKey(state.focusEdit.element)
if (!content || (content.trim() === '')) {
if (state.focusEdit.isNew) {
const files = removePath(state.focusEdit.element, state.files)
const updatedFiles = files.filter(file => file)
setState(prevState => {
return { ...prevState, files: updatedFiles, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
} else {
editRef.current.textContent = state.focusEdit.lastEdit
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
} else {
if (state.focusEdit.lastEdit === content) {
return setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
if (helper.checkSpecialChars(content)) {
modal('Validation Error', 'Special characters are not allowed', {
label: 'Ok',
fn: () => {}
}, null)
} else {
if (state.focusEdit.isNew) {
state.focusEdit.type === 'file' ? createNewFile(parentFolder + '/' + content) : createNewFolder(parentFolder + '/' + content)
const files = removePath(state.focusEdit.element, state.files)
const updatedFiles = files.filter(file => file)
setState(prevState => {
return { ...prevState, files: updatedFiles }
})
} else {
const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath)
const newPath = oldPath.replace(oldName, content)
editRef.current.textContent = extractNameFromKey(oldPath)
renamePath(oldPath, newPath)
}
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
}
}
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
const expandPath = [...new Set([...state.expandPath, parentFolder])]
setState(prevState => {
return { ...prevState, expandPath }
})
editModeOn(parentFolder + '/blank', 'file', true)
}
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
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder)
const expandPath = [...new Set([...state.expandPath, parentFolder])]
setState(prevState => {
return { ...prevState, expandPath }
})
editModeOn(parentFolder + '/blank', 'folder', true)
}
const handleEditInput = (event) => {
if (event.which === 13) {
event.preventDefault()
editModeOff(editRef.current.innerText)
}
}
const label = (file: File) => {
return (
<div
className='remixui_items d-inline-block w-100'
ref={state.focusEdit.element === file.path ? editRef : null}
suppressContentEditableWarning={true}
contentEditable={state.focusEdit.element === file.path}
onKeyDown={handleEditInput}
onBlur={(e) => {
e.stopPropagation()
editModeOff(editRef.current.innerText)
}}
>
<span
title={file.path}
className={'remixui_label ' + (file.isDirectory ? 'folder' : 'remixui_leaf')}
data-path={file.path}
>
{ file.name }
</span>
</div>
)
}
const renderFiles = (file: File, index: number) => {
if (file.isDirectory) {
return (
<div key={index}>
<TreeViewItem
id={`treeViewItem${file.path}`}
iconX='pr-3 fa fa-folder'
iconY='pr-3 fa fa-folder-open'
key={`${file.path + index}`}
label={label(file)}
onClick={(e) => {
e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFolder(file.path)
}}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
handleContextMenuFolder(e.pageX, e.pageY, file.path, e.target.textContent)
}}
labelClass={ state.focusEdit.element === file.path ? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1 ? 'bg-secondary' : '' }
controlBehaviour={ state.ctrlKey }
expand={state.expandPath.includes(file.path)}
>
{
file.child ? <TreeView id={`treeView${file.path}`} key={index}>{
file.child.map((file, index) => {
return renderFiles(file, index)
})
}
</TreeView> : <TreeView id={`treeView${file.path}`} key={index} />
}
</TreeViewItem>
{ ((state.focusContext.element === file.path) && (state.focusEdit.element !== file.path)) &&
<FileExplorerContextMenu
actions={state.actions}
hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
renamePath={editModeOn}
extractParentFromKey={extractParentFromKey}
publishToGist={publishToGist}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
path={file.path}
type='folder'
/>
}
</div>
)
} else {
return (
<div key={index}>
<TreeViewItem
id={`treeViewItem${file.path}`}
key={index}
label={label(file)}
onClick={(e) => {
e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFile(file.path)
}}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
handleContextMenuFile(e.pageX, e.pageY, file.path, e.target.textContent)
}}
icon='far fa-file'
labelClass={ state.focusEdit.element === file.path ? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1 ? 'bg-secondary' : '' }
/>
{ ((state.focusContext.element === file.path) && (state.focusEdit.element !== file.path)) &&
<FileExplorerContextMenu
actions={state.actions}
hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
renamePath={editModeOn}
runScript={runScript}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
path={file.path}
type='file'
/>
}
</div>
)
}
}
return (
<div>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem"
label={
<FileExplorerMenu
title={name}
menuItems={props.menuItems}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
publishToGist={publishToGist}
uploadFile={uploadFile}
fileManager={state.fileManager}
/>
}
expand={true}>
<div className='pb-2'>
<TreeView id='treeViewMenu'>
{
state.files.map((file, index) => {
return renderFiles(file, index)
})
}
</TreeView>
</div>
</TreeViewItem>
</TreeView>
{
props.name && <ModalDialog
id={ props.name }
title={ state.modalOptions.title }
message={ state.modalOptions.message }
hide={ state.modalOptions.hide }
ok={ state.modalOptions.ok }
cancel={ state.modalOptions.cancel }
handleHide={ handleHideModal }
/>
}
<Toaster message={state.toasterMsg} />
</div>
)
}
export default FileExplorer
function packageFiles (filesProvider, directory, callback) {
const ret = {}
filesProvider.resolveDirectory(directory, (error, files) => {
if (error) callback(error)
else {
async.eachSeries(Object.keys(files), (path, cb) => {
if (filesProvider.isDirectory(path)) {
cb()
} else {
filesProvider.get(path, (error, content) => {
if (error) return cb(error)
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
ret[path] = { content }
cb()
})
}
}, (error) => {
callback(error, ret)
})
}
})
}

@ -0,0 +1,41 @@
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
registry: any,
filesProvider: any,
menuItems?: string[],
plugin: any
}
export interface File {
path: string,
name: string,
isDirectory: boolean,
child?: File[]
}
export interface FileExplorerMenuProps {
title: string,
menuItems: string[],
fileManager: any,
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
publishToGist: () => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void
}
export interface FileExplorerContextMenuProps {
actions: { name: string, type: string[], path: string[], extension: string[], pattern: string[] }[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
deletePath: (path: string) => void,
renamePath: (path: string, type: string) => void,
hideContextMenu: () => void,
extractParentFromKey?: (key: string) => string,
publishToGist?: () => void,
runScript?: (path: string) => void,
pageX: number,
pageY: number,
path: string,
type: string
}

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./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"]
}

@ -9,14 +9,12 @@ export const ModalDialog = (props: ModalDialogProps) => {
})
const modal = useRef(null)
const handleHide = () => {
props.hide()
props.handleHide()
}
useEffect(
() => {
modal.current.focus()
}, []
)
useEffect(() => {
modal.current.focus()
}, [props.hide])
const modalKeyEvent = (keyCode) => {
if (keyCode === 27) { // Esc
@ -41,74 +39,72 @@ export const ModalDialog = (props: ModalDialogProps) => {
}
handleHide()
}
return (<>
return (
<div
id="modal-dialog"
data-id="modalDialogContainer"
data-id={`${props.id}ModalDialogContainer-react`}
data-backdrop="static"
data-keyboard="false"
tabIndex={-1}
className="modal d-block"
className='modal'
style={{ display: props.hide ? 'none' : 'block' }}
role="dialog"
>
<div id="modal-background" className="modal-dialog" role="document">
<div className="modal-dialog" role="document">
<div
tabIndex={1}
onBlur={(e) => {
e.stopPropagation()
handleHide()
}}
ref={modal}
className={'modal-content remixModalContent ' + (props.opts ? props.opts.class ? props.opts.class : '' : '')}
tabIndex={-1}
className={'modal-content remixModalContent ' + (props.modalClass ? props.modalClass : '')}
onKeyDown={({ keyCode }) => { modalKeyEvent(keyCode) }}
>
<div className="modal-header">
<h6 className="modal-title" data-id="modalDialogModalTitle">
<h6 className="modal-title" data-id={`${props.id}ModalDialogModalTitle-react`}>
{props.title && props.title}
</h6>
{!props.opts.hideClose &&
{!props.showCancelIcon &&
<span className="modal-close" onClick={() => handleHide()}>
<i id="modal-close" title="Close" className="fas fa-times" aria-hidden="true"></i>
<i title="Close" className="fas fa-times" aria-hidden="true"></i>
</span>
}
</div>
<div className="modal-body text-break remixModalBody" data-id="modalDialogModalBody">
{props.content &&
props.content
}
<div className="modal-body text-break remixModalBody" data-id={`${props.id}ModalDialogModalBody-react`}>
{ props.children ? props.children : props.message }
</div>
<div className="modal-footer" data-id="modalDialogModalFooter">
<div className="modal-footer" data-id={`${props.id}ModalDialogModalFooter-react`}>
{/* todo add autofocus ^^ */}
{props.ok &&
<span
id="modal-footer-ok"
className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')}
onClick={() => {
if (props.ok && props.ok.fn) props.ok.fn()
handleHide()
}}
tabIndex={1}
>
{props.ok && props.ok.label ? props.ok.label : 'OK'}
</span>
{ props.ok &&
<span
data-id={`${props.id}-modal-footer-ok-react`}
className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')}
onClick={() => {
if (props.ok.fn) props.ok.fn()
handleHide()
}}
>
{ props.ok.label ? props.ok.label : 'OK' }
</span>
}
{ props.cancel &&
<span
data-id={`${props.id}-modal-footer-cancel-react`}
className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')}
data-dismiss="modal"
onClick={() => {
if (props.cancel.fn) props.cancel.fn()
handleHide()
}}
>
{ props.cancel.label ? props.cancel.label : 'Cancel' }
</span>
}
<span
id="modal-footer-cancel"
className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')}
data-dismiss="modal"
onClick={() => {
if (props.cancel && props.cancel.fn) props.cancel.fn()
handleHide()
}}
tabIndex={2}
>
{props.cancel && props.cancel.label ? props.cancel.label : 'Cancel'}
</span>
</div>
</div>
</div>
</div>
</>)
)
}
export default ModalDialog

@ -1,9 +1,12 @@
export interface ModalDialogProps {
id?: string
title?: string,
content?: JSX.Element,
ok?: {label:string, fn: () => void},
cancel?: {label:string, fn: () => void},
focusSelector?: string,
opts?: {class: string, hideClose?: boolean},
hide: () => void
message?: string,
ok?: { label: string, fn: () => void },
cancel: { label: string, fn: () => void },
modalClass?: string,
showCancelIcon?: boolean,
hide: boolean,
handleHide: (hideState?: boolean) => void,
children?: React.ReactNode
}

@ -89,7 +89,15 @@ export const Toaster = (props: ToasterProps) => {
return (
<>
{/* <ModalDialog /> */}
<ModalDialog
message={props.message}
cancel={{
label: 'Close',
fn: () => {}
}}
hide={!state.showModal}
handleHide={hideFullMessage}
/>
{ !state.hide &&
<div data-shared="tooltipPopup" className={`remixui_tooltip alert alert-info p-2 ${state.hiding ? 'remixui_animateTop' : 'remixui_animateBottom'}`} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<span className="px-2">

@ -1,248 +1,19 @@
{
"rules": {
"array-callback-return": "warn",
"dot-location": ["warn", "property"],
"eqeqeq": ["warn", "smart"],
"new-parens": "warn",
"no-caller": "warn",
"no-cond-assign": ["warn", "except-parens"],
"no-const-assign": "warn",
"no-control-regex": "warn",
"no-delete-var": "warn",
"no-dupe-args": "warn",
"no-dupe-keys": "warn",
"no-duplicate-case": "warn",
"no-empty-character-class": "warn",
"no-empty-pattern": "warn",
"no-eval": "warn",
"no-ex-assign": "warn",
"no-extend-native": "warn",
"no-extra-bind": "warn",
"no-extra-label": "warn",
"no-fallthrough": "warn",
"no-func-assign": "warn",
"no-implied-eval": "warn",
"no-invalid-regexp": "warn",
"no-iterator": "warn",
"no-label-var": "warn",
"no-labels": ["warn", { "allowLoop": true, "allowSwitch": false }],
"no-lone-blocks": "warn",
"no-loop-func": "warn",
"no-mixed-operators": [
"warn",
{
"groups": [
["&", "|", "^", "~", "<<", ">>", ">>>"],
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["&&", "||"],
["in", "instanceof"]
],
"allowSamePrecedence": false
}
],
"no-multi-str": "warn",
"no-native-reassign": "warn",
"no-negated-in-lhs": "warn",
"no-new-func": "warn",
"no-new-object": "warn",
"no-new-symbol": "warn",
"no-new-wrappers": "warn",
"no-obj-calls": "warn",
"no-octal": "warn",
"no-octal-escape": "warn",
"no-redeclare": "warn",
"no-regex-spaces": "warn",
"no-restricted-syntax": ["warn", "WithStatement"],
"no-script-url": "warn",
"no-self-assign": "warn",
"no-self-compare": "warn",
"no-sequences": "warn",
"no-shadow-restricted-names": "warn",
"no-sparse-arrays": "warn",
"no-template-curly-in-string": "warn",
"no-this-before-super": "warn",
"no-throw-literal": "warn",
"no-restricted-globals": [
"error",
"addEventListener",
"blur",
"close",
"closed",
"confirm",
"defaultStatus",
"defaultstatus",
"event",
"external",
"find",
"focus",
"frameElement",
"frames",
"history",
"innerHeight",
"innerWidth",
"length",
"location",
"locationbar",
"menubar",
"moveBy",
"moveTo",
"name",
"onblur",
"onerror",
"onfocus",
"onload",
"onresize",
"onunload",
"open",
"opener",
"opera",
"outerHeight",
"outerWidth",
"pageXOffset",
"pageYOffset",
"parent",
"print",
"removeEventListener",
"resizeBy",
"resizeTo",
"screen",
"screenLeft",
"screenTop",
"screenX",
"screenY",
"scroll",
"scrollbars",
"scrollBy",
"scrollTo",
"scrollX",
"scrollY",
"self",
"status",
"statusbar",
"stop",
"toolbar",
"top"
],
"no-unexpected-multiline": "warn",
"no-unreachable": "warn",
"no-unused-expressions": [
"error",
{
"allowShortCircuit": true,
"allowTernary": true,
"allowTaggedTemplates": true
}
],
"no-unused-labels": "warn",
"no-useless-computed-key": "warn",
"no-useless-concat": "warn",
"no-useless-escape": "warn",
"no-useless-rename": [
"warn",
{
"ignoreDestructuring": false,
"ignoreImport": false,
"ignoreExport": false
}
],
"no-with": "warn",
"no-whitespace-before-property": "warn",
"react-hooks/exhaustive-deps": "warn",
"require-yield": "warn",
"rest-spread-spacing": ["warn", "never"],
"strict": ["warn", "never"],
"unicode-bom": ["warn", "never"],
"use-isnan": "warn",
"valid-typeof": "warn",
"no-restricted-properties": [
"error",
{
"object": "require",
"property": "ensure",
"message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting"
},
{
"object": "System",
"property": "import",
"message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting"
}
],
"getter-return": "warn",
"import/first": "error",
"import/no-amd": "error",
"import/no-webpack-loader-syntax": "error",
"react/forbid-foreign-prop-types": ["warn", { "allowInPropTypes": true }],
"react/jsx-no-comment-textnodes": "warn",
"react/jsx-no-duplicate-props": "warn",
"react/jsx-no-target-blank": "warn",
"react/jsx-no-undef": "error",
"react/jsx-pascal-case": ["warn", { "allowAllCaps": true, "ignore": [] }],
"react/jsx-uses-react": "warn",
"react/jsx-uses-vars": "warn",
"react/no-danger-with-children": "warn",
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-typos": "error",
"react/react-in-jsx-scope": "error",
"react/require-render-return": "error",
"react/style-prop-object": "warn",
"react/jsx-no-useless-fragment": "warn",
"jsx-a11y/accessible-emoji": "warn",
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "warn",
"jsx-a11y/anchor-is-valid": [
"warn",
{ "aspects": ["noHref", "invalidHref"] }
],
"jsx-a11y/aria-activedescendant-has-tabindex": "warn",
"jsx-a11y/aria-props": "warn",
"jsx-a11y/aria-proptypes": "warn",
"jsx-a11y/aria-role": "warn",
"jsx-a11y/aria-unsupported-elements": "warn",
"jsx-a11y/heading-has-content": "warn",
"jsx-a11y/iframe-has-title": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"jsx-a11y/no-access-key": "warn",
"jsx-a11y/no-distracting-elements": "warn",
"jsx-a11y/no-redundant-roles": "warn",
"jsx-a11y/role-has-required-aria-props": "warn",
"jsx-a11y/role-supports-aria-props": "warn",
"jsx-a11y/scope": "warn",
"react-hooks/rules-of-hooks": "error",
"default-case": "off",
"no-dupe-class-members": "off",
"no-undef": "off",
"@typescript-eslint/consistent-type-assertions": "warn",
"no-array-constructor": "off",
"@typescript-eslint/no-array-constructor": "warn",
"@typescript-eslint/no-namespace": "error",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": [
"warn",
{
"functions": false,
"classes": false,
"variables": false,
"typedefs": false
}
],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ "args": "none", "ignoreRestSiblings": true }
],
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "warn"
},
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"jest": true,
"node": true
"browser": true,
"es6": true
},
"extends": "../../../.eslintrc",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"settings": { "react": { "version": "detect" } },
"plugins": ["import", "jsx-a11y", "react", "react-hooks"],
"extends": ["../../../.eslintrc"],
"ignorePatterns": ["!**/*"]
}
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
}
}

@ -9,4 +9,4 @@ module.exports = {
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
coverageDirectory: '../../../coverage/libs/remix-ui/tree-view'
};
}

@ -1,2 +1,2 @@
export * from './lib/tree-view-item/tree-view-item';
export * from './lib/remix-ui-tree-view';
export * from './lib/tree-view-item/tree-view-item'
export * from './lib/remix-ui-tree-view'

@ -1,11 +1,11 @@
import React from 'react'
import React from 'react' // eslint-disable-line
import { TreeViewProps } from '../types'
import './remix-ui-tree-view.css'
export const TreeView = (props: TreeViewProps) => {
const { children, id, ...otherProps } = props
return (
<ul data-id={`treeViewUl${id}`} className="ul_tv ml-0 px-2" { ...otherProps }>
{ children }

@ -1,10 +1,10 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect } from 'react' // eslint-disable-line
import { TreeViewItemProps } from '../../types'
import './tree-view-item.css'
export const TreeViewItem = (props: TreeViewItemProps) => {
const { id, children, label, expand, ...otherProps } = props
const { id, children, label, labelClass, expand, iconX = 'fas fa-caret-right', iconY = 'fas fa-caret-down', icon, controlBehaviour = false, innerRef, ...otherProps } = props
const [isExpanded, setIsExpanded] = useState(false)
useEffect(() => {
@ -12,14 +12,14 @@ export const TreeViewItem = (props: TreeViewItemProps) => {
}, [expand])
return (
<li key={`treeViewLi${id}`} data-id={`treeViewLi${id}`} className='li_tv' {...otherProps}>
<div key={`treeViewDiv${id}`} data-id={`treeViewDiv${id}`} className='d-flex flex-row align-items-center' onClick={() => setIsExpanded(!isExpanded)}>
<div className={isExpanded ? 'px-1 fas fa-caret-down caret caret_tv' : 'px-1 fas fa-caret-right caret caret_tv'} style={{ visibility: children ? 'visible' : 'hidden' }}></div>
<li ref={innerRef} key={`treeViewLi${id}`} data-id={`treeViewLi${id}`} className='li_tv' {...otherProps}>
<div key={`treeViewDiv${id}`} data-id={`treeViewDiv${id}`} className={`d-flex flex-row align-items-center ${labelClass}`} onClick={() => !controlBehaviour && setIsExpanded(!isExpanded)}>
{ children ? <div className={isExpanded ? `px-1 ${iconY} caret caret_tv` : `px-1 ${iconX} caret caret_tv`} style={{ visibility: children ? 'visible' : 'hidden' }}></div> : icon ? <div className={`pr-3 pl-1 ${icon} caret caret_tv`}></div> : null }
<span className='w-100 pl-1'>
{ label }
</span>
</div>
{ isExpanded ? children : null }
{ isExpanded ? children : null }
</li>
)
}

@ -1,13 +1,22 @@
export interface TreeViewProps {
children?: React.ReactNode,
id: string
id?: string
}
export interface TreeViewItemProps {
children?: React.ReactNode,
id: string,
id?: string,
label: string | number | React.ReactNode,
expand?: boolean,
onClick?: VoidFunction,
className?: string
}
onClick?: (...args: any) => void,
onInput?: (...args: any) => void,
className?: string,
iconX?: string,
iconY?: string,
icon?: string,
labelClass?: string,
controlBehaviour?: boolean
innerRef?: any,
onContextMenu?: (...args: any) => void,
onBlur?: (...args: any) => void
}

@ -7,7 +7,7 @@ import * as fs from 'fs-extra'
import * as isbinaryfile from 'isbinaryfile'
export class RemixdClient extends PluginClient {
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'list', 'isDirectory']
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'list', 'isDirectory', 'createDir']
trackDownStreamUpdate: TrackDownStreamUpdate = {}
websocket: WS
currentSharedFolder: string
@ -127,6 +127,30 @@ export class RemixdClient extends PluginClient {
}
}
createDir (args: SharedFolderArgs): Promise<void> {
try {
return new Promise((resolve, reject) => {
if (this.readOnly) 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)) 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))
})
} catch (error) {
throw new Error(error)
}
}
rename (args: SharedFolderArgs): Promise<boolean> {
try {
return new Promise((resolve, reject) => {

@ -86,6 +86,12 @@
},
"remix-ui-toaster": {
"tags": []
},
"remix-ui-file-explorer": {
"tags": []
},
"debugger": {
"tags": []
}
}
}

1005
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -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=remixd,remix-ui-modal-dialog,remix-ui-toaster",
"lint:libs": "nx run-many --target=lint --projects=remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer",
"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",
@ -82,6 +82,8 @@
"nightwatch_local_fileManager": "npm run build:e2e & nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/fileManager_api.test.js --env=chrome",
"nightwatch_local_runAndDeploy": "npm run build:e2e & nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/runAndDeploy.js --env=chrome-runAndDeploy",
"nightwatch_local_url": "npm run build:e2e & nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/url.test.js --env=chrome",
"nightwatch_local_verticalIconscontextmenu": "npm run build:e2e & nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/verticalIconsPanel.test.js --env=chrome",
"onchange": "onchange apps/remix-ide/build/app.js -- npm-run-all lint",
"remixd": "nx build remixd & nx serve remixd --folder=./apps/remix-ide/contracts --remixide=http://127.0.0.1:8080",
"selenium": "selenium-standalone start",
@ -153,12 +155,14 @@
"merge": "^1.2.0",
"npm-install-version": "^6.0.2",
"react": "16.13.1",
"react-beautiful-dnd": "^13.0.0",
"react-bootstrap": "^1.3.0",
"react-dom": "16.13.1",
"signale": "^1.4.0",
"time-stamp": "^2.2.0",
"winston": "^3.3.3",
"ws": "^7.3.0"
"ws": "^7.3.0",
"document-register-element": "1.13.1"
},
"devDependencies": {
"@babel/core": "^7.4.5",
@ -190,6 +194,7 @@
"@types/nightwatch": "^1.1.6",
"@types/node": "~8.9.4",
"@types/react": "16.9.17",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "16.9.4",
"@types/react-router-dom": "5.1.3",
"@types/ws": "^7.2.4",
@ -281,6 +286,7 @@
"worker-loader": "^2.0.0",
"yo-yo": "github:ioedeveloper/yo-yo",
"yo-yoify": "^3.7.3",
"@types/jest": "25.1.4"
"@types/jest": "25.1.4",
"@testing-library/react": "10.4.1"
}
}

@ -26,6 +26,7 @@
"@remix-project/remix-url-resolver": [
"dist/libs/remix-url-resolver/index.js"
],
"@remixproject/debugger-plugin": ["apps/debugger/src/index.ts"],
"@remix-project/remixd": ["dist/libs/remixd/index.js"],
"@remix-ui/tree-view": ["libs/remix-ui/tree-view/src/index.ts"],
"@remix-ui/debugger-ui": ["libs/remix-ui/debugger-ui/src/index.ts"],
@ -33,7 +34,8 @@
"@remix-ui/clipboard": ["libs/remix-ui/clipboard/src/index.ts"],
"@remix-project/remix-solidity-ts": ["libs/remix-solidity/src/index.ts"],
"@remix-ui/modal-dialog": ["libs/remix-ui/modal-dialog/src/index.ts"],
"@remix-ui/toaster": ["libs/remix-ui/toaster/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"]
}
},
"exclude": ["node_modules", "tmp"]

@ -0,0 +1,65 @@
{
"rulesDirectory": ["node_modules/@nrwl/workspace/src/tslint"],
"linterOptions": {
"exclude": ["**/*"]
},
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"deprecation": {
"severity": "warn"
},
"forin": true,
"import-blacklist": [true, "rxjs/Rx"],
"interface-over-type-literal": true,
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [true, "ignore-params"],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"prefer-const": true,
"radix": true,
"triple-equals": [true, "allow-null-check"],
"unified-signatures": true,
"variable-name": false,
"nx-enforce-module-boundaries": [
true,
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{ "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
]
}
]
}
}

@ -617,6 +617,94 @@
}
}
}
},
"remix-ui-file-explorer": {
"root": "libs/remix-ui/file-explorer",
"sourceRoot": "libs/remix-ui/file-explorer/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/file-explorer/tsconfig.lib.json"],
"exclude": [
"**/node_modules/**",
"!libs/remix-ui/file-explorer/**/*"
]
}
}
}
},
"debugger": {
"root": "apps/debugger",
"sourceRoot": "apps/debugger/src",
"projectType": "application",
"schematics": {},
"architect": {
"build": {
"builder": "@nrwl/web:build",
"options": {
"outputPath": "dist/apps/debugger",
"index": "apps/debugger/src/index.html",
"main": "apps/debugger/src/main.tsx",
"polyfills": "apps/debugger/src/polyfills.ts",
"tsConfig": "apps/debugger/tsconfig.app.json",
"assets": [
"apps/debugger/src/assets",
"apps/debugger/src/index.html",
"apps/debugger/src/favicon.ico"
],
"styles": [],
"scripts": [],
"webpackConfig": "apps/debugger/webpack.config.js",
"maxWorkers": 2
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/debugger/src/environments/environment.ts",
"with": "apps/debugger/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@nrwl/web:dev-server",
"options": {
"buildTarget": "debugger:build"
},
"configurations": {
"production": {
"buildTarget": "debugger:build:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["apps/debugger/tsconfig.app.json"],
"exclude": ["**/node_modules/**", "!apps/debugger/**/*"]
}
}
}
}
},
"cli": {

Loading…
Cancel
Save