Merge branch 'master' into fix-update-sourcify-plugin-name

pull/5370/head
David Disu 3 years ago committed by GitHub
commit 73effb57cd
  1. 14
      apps/remix-ide-e2e/nightwatch.ts
  2. 15
      apps/remix-ide-e2e/src/commands/modalFooterCancelClick.ts
  3. 11
      apps/remix-ide-e2e/src/commands/modalFooterOKClick.ts
  4. 1
      apps/remix-ide-e2e/src/helpers/init.ts
  5. 38
      apps/remix-ide-e2e/src/tests/gist.test.ts
  6. 4
      apps/remix-ide-e2e/src/tests/importFromGithub.test.ts
  7. 4
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  8. 4
      apps/remix-ide-e2e/src/types/index.d.ts
  9. 182
      apps/remix-ide/src/app.js
  10. 31
      apps/remix-ide/src/app/components/hidden-panel.js
  11. 37
      apps/remix-ide/src/app/components/hidden-panel.tsx
  12. 38
      apps/remix-ide/src/app/components/main-panel.js
  13. 57
      apps/remix-ide/src/app/components/main-panel.tsx
  14. 111
      apps/remix-ide/src/app/components/panel.js
  15. 63
      apps/remix-ide/src/app/components/panel.ts
  16. 156
      apps/remix-ide/src/app/components/side-panel.js
  17. 95
      apps/remix-ide/src/app/components/side-panel.tsx
  18. 2
      apps/remix-ide/src/app/components/vertical-icons.js
  19. 48
      apps/remix-ide/src/app/files/fileManager.ts
  20. 2
      apps/remix-ide/src/app/panels/file-panel.js
  21. 94
      apps/remix-ide/src/app/panels/layout.ts
  22. 204
      apps/remix-ide/src/app/panels/main-view.js
  23. 18
      apps/remix-ide/src/app/panels/tab-proxy.js
  24. 5
      apps/remix-ide/src/app/panels/terminal.js
  25. 44
      apps/remix-ide/src/app/plugins/modal.tsx
  26. 143
      apps/remix-ide/src/app/plugins/remixd-handle.tsx
  27. 4
      apps/remix-ide/src/app/udapp/run-tab.js
  28. 3
      apps/remix-ide/src/app/ui/landing-page/landing-page.js
  29. 34
      apps/remix-ide/src/framingService.js
  30. 5
      apps/remix-ide/src/lib/cmdInterpreterAPI.js
  31. 74
      apps/remix-ide/src/lib/gist-handler.js
  32. 88
      apps/remix-ide/src/lib/panels-resize.js
  33. 8
      apps/remix-ide/src/remixAppManager.js
  34. 16
      apps/remix-ide/test/compiler-test.js
  35. 52
      apps/remix-ide/test/gist-handler-test.js
  36. 5
      apps/remix-ide/test/index.js
  37. 23
      apps/remix-ide/test/query-params-test.js
  38. 2
      apps/remix-ide/tsconfig.json
  39. 1
      libs/remix-core-plugin/src/index.ts
  40. 2
      libs/remix-core-plugin/src/lib/compiler-content-imports.ts
  41. 138
      libs/remix-core-plugin/src/lib/gist-handler.ts
  42. 5
      libs/remix-ui/app/src/index.ts
  43. 30
      libs/remix-ui/app/src/lib/remix-app/actions/modals.ts
  44. 27
      libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.css
  45. 21
      libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx
  46. 15
      libs/remix-ui/app/src/lib/remix-app/components/modals/dialogViewPlugin.tsx
  47. 16
      libs/remix-ui/app/src/lib/remix-app/components/modals/dialogs.tsx
  48. 45
      libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx
  49. 56
      libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx
  50. 32
      libs/remix-ui/app/src/lib/remix-app/components/modals/origin-warning.tsx
  51. 0
      libs/remix-ui/app/src/lib/remix-app/components/splashscreen.tsx
  52. 23
      libs/remix-ui/app/src/lib/remix-app/context/context.tsx
  53. 64
      libs/remix-ui/app/src/lib/remix-app/context/provider.tsx
  54. 26
      libs/remix-ui/app/src/lib/remix-app/dragbar/dragbar.css
  55. 29
      libs/remix-ui/app/src/lib/remix-app/interface/index.ts
  56. 51
      libs/remix-ui/app/src/lib/remix-app/modals/matomo.tsx
  57. 50
      libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts
  58. 45
      libs/remix-ui/app/src/lib/remix-app/remix-app.tsx
  59. 17
      libs/remix-ui/app/src/lib/remix-app/state/modals.ts
  60. 7
      libs/remix-ui/app/src/lib/remix-app/types/index.ts
  61. 2
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx
  62. 66
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  63. 2
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  64. 4
      libs/remix-ui/panel/.babelrc
  65. 18
      libs/remix-ui/panel/.eslintrc.json
  66. 7
      libs/remix-ui/panel/README.md
  67. 2
      libs/remix-ui/panel/src/index.ts
  68. 27
      libs/remix-ui/panel/src/lib/dragbar/dragbar.css
  69. 51
      libs/remix-ui/panel/src/lib/dragbar/dragbar.tsx
  70. 8
      libs/remix-ui/panel/src/lib/main/main-panel.css
  71. 60
      libs/remix-ui/panel/src/lib/main/main-panel.tsx
  72. 27
      libs/remix-ui/panel/src/lib/plugins/panel-header.tsx
  73. 37
      libs/remix-ui/panel/src/lib/plugins/panel-plugin.tsx
  74. 110
      libs/remix-ui/panel/src/lib/plugins/panel.css
  75. 29
      libs/remix-ui/panel/src/lib/plugins/remix-ui-panel.tsx
  76. 9
      libs/remix-ui/panel/src/lib/types/index.ts
  77. 20
      libs/remix-ui/panel/tsconfig.json
  78. 13
      libs/remix-ui/panel/tsconfig.lib.json
  79. 3
      libs/remix-ui/plugin-manager/src/types.d.ts
  80. 2
      libs/remix-ui/solidity-unit-testing/.babelrc
  81. 3
      libs/remix-ui/solidity-unit-testing/.eslintrc.json
  82. 14
      libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts
  83. 178
      libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
  84. 4
      libs/remix-ui/terminal/src/lib/actions/terminalAction.ts
  85. 77
      libs/remix-ui/terminal/src/lib/custom-hooks/useDragTerminal.tsx
  86. 38
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx
  87. 6
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  88. 6
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  89. 3
      nx.json
  90. 4
      package.json
  91. 5
      tsconfig.base.json
  92. 25
      workspace.json

@ -68,7 +68,13 @@ module.exports = {
desiredCapabilities: { desiredCapabilities: {
browserName: 'firefox', browserName: 'firefox',
javascriptEnabled: true, javascriptEnabled: true,
acceptSslCerts: true acceptSslCerts: true,
'moz:firefoxOptions': {
args: [
'-width=2560',
'-height=1440'
]
}
} }
}, },
@ -78,7 +84,11 @@ module.exports = {
javascriptEnabled: true, javascriptEnabled: true,
acceptSslCerts: true, acceptSslCerts: true,
'moz:firefoxOptions': { 'moz:firefoxOptions': {
args: ['-headless'] args: [
'-headless',
'-width=2560',
'-height=1440'
]
} }
} }
} }

@ -1,14 +1,15 @@
import { NightwatchBrowser } from 'nightwatch' import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events' import EventEmitter from 'events'
class ModalFooterOKClick extends EventEmitter { class ModalFooterCancelClick extends EventEmitter {
command (this: NightwatchBrowser): NightwatchBrowser { command (this: NightwatchBrowser, id?: string): NightwatchBrowser {
this.api.waitForElementVisible('#modal-footer-cancel').perform((client, done) => { const clientId = id ? `*[data-id="${id}-modal-footer-cancel-react"]` : '#modal-footer-cancel'
this.api.execute(function () { this.api.waitForElementVisible(clientId).perform((client, done) => {
const elem = document.querySelector('#modal-footer-cancel') as HTMLElement this.api.execute(function (clientId) {
const elem = document.querySelector(clientId) as HTMLElement
elem.click() elem.click()
}, [], () => { }, [clientId], () => {
done() done()
this.emit('complete') this.emit('complete')
}) })
@ -17,4 +18,4 @@ class ModalFooterOKClick extends EventEmitter {
} }
} }
module.exports = ModalFooterOKClick module.exports = ModalFooterCancelClick

@ -2,13 +2,14 @@ import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events' import EventEmitter from 'events'
class ModalFooterOKClick extends EventEmitter { class ModalFooterOKClick extends EventEmitter {
command (this: NightwatchBrowser): NightwatchBrowser { command (this: NightwatchBrowser, id?: string): NightwatchBrowser {
this.api.waitForElementVisible('#modal-footer-ok').perform((client, done) => { const clientId = id ? `*[data-id="${id}-modal-footer-ok-react"]` : '#modal-footer-ok'
this.api.execute(function () { this.api.waitForElementVisible(clientId).perform((client, done) => {
const elem = document.querySelector('#modal-footer-ok') as HTMLElement this.api.execute(function (clientId) {
const elem = document.querySelector(clientId) as HTMLElement
elem.click() elem.click()
}, [], () => { }, [clientId], () => {
done() done()
this.emit('complete') this.emit('complete')
}) })

@ -9,6 +9,7 @@ export default function (browser: NightwatchBrowser, callback: VoidFunction, url
.switchBrowserTab(0) .switchBrowserTab(0)
.waitForElementVisible('[id="remixTourSkipbtn"]') .waitForElementVisible('[id="remixTourSkipbtn"]')
.click('[id="remixTourSkipbtn"]') .click('[id="remixTourSkipbtn"]')
.maximizeWindow()
.fullscreenWindow(() => { .fullscreenWindow(() => {
if (preloadPlugins) { if (preloadPlugins) {
initModules(browser, () => { initModules(browser, () => {

@ -76,29 +76,34 @@ module.exports = {
.waitForElementVisible('button[data-id="landingPageImportFromGistButton"]') .waitForElementVisible('button[data-id="landingPageImportFromGistButton"]')
.pause(1000) .pause(1000)
.scrollAndClick('button[data-id="landingPageImportFromGistButton"]') .scrollAndClick('button[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogModalTitle"]') .waitForElementVisible('*[data-id="gisthandlerModalDialogModalTitle-react"]')
.assert.containsText('*[data-id="modalDialogModalTitle"]', 'Load a Gist') .assert.containsText('*[data-id="gisthandlerModalDialogModalTitle-react"]', 'Load a Gist')
.waitForElementVisible('*[data-id="modalDialogModalBody"]') .waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"]')
.assert.containsText('*[data-id="modalDialogModalBody"]', 'Enter the ID of the Gist or URL you would like to load.') .assert.containsText('*[data-id="gisthandlerModalDialogModalBody-react"]', 'Enter the ID of the Gist or URL you would like to load.')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]') .waitForElementVisible('*[data-id="modalDialogCustomPromp"]')
.modalFooterCancelClick() .modalFooterCancelClick('gisthandler')
}, },
'Display Error Message For Invalid Gist ID': function (browser: NightwatchBrowser) { 'Display Error Message For Invalid Gist ID': function (browser: NightwatchBrowser) {
browser browser
.pause(1000)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGistButton"]') .scrollAndClick('*[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]') .waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', testData.invalidGistId) .execute(() => {
.modalFooterOKClick() (document.querySelector('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]') as any).focus()
.waitForElementVisible('*[data-id="modalDialogModalBody"]') }, [], () => {})
.assert.containsText('*[data-id="modalDialogModalBody"]', 'Not Found') .setValue('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]', testData.invalidGistId)
.modalFooterOKClick() .modalFooterOKClick('gisthandler')
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"]')
.assert.containsText('*[data-id="gisthandlerModalDialogModalBody-react"]', 'Not Found')
.modalFooterOKClick('gisthandler')
}, },
'Display Error Message For Missing Gist Token When Publishing': function (browser: NightwatchBrowser) { 'Display Error Message For Missing Gist Token When Publishing': function (browser: NightwatchBrowser) {
browser browser
.pause(1000)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings') .clickLaunchIcon('settings')
.waitForElementVisible('[data-id="settingsTabRemoveGistToken"]') .waitForElementVisible('[data-id="settingsTabRemoveGistToken"]')
@ -129,9 +134,12 @@ module.exports = {
.click('[data-id="settingsTabSaveGistToken"]') .click('[data-id="settingsTabSaveGistToken"]')
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGistButton"]') .scrollAndClick('*[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]') .waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', testData.validGistId) .execute(() => {
.modalFooterOKClick() (document.querySelector('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]') as any).focus()
}, [], () => {})
.setValue('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]', testData.validGistId)
.modalFooterOKClick('gisthandler')
.openFile(`gist-${testData.validGistId}/README.txt`) .openFile(`gist-${testData.validGistId}/README.txt`)
.waitForElementVisible(`div[title='default_workspace/gist-${testData.validGistId}/README.txt']`) .waitForElementVisible(`div[title='default_workspace/gist-${testData.validGistId}/README.txt']`)
.assert.containsText(`div[title='default_workspace/gist-${testData.validGistId}/README.txt'] > span`, 'README.txt') .assert.containsText(`div[title='default_workspace/gist-${testData.validGistId}/README.txt'] > span`, 'README.txt')

@ -31,8 +31,6 @@ module.exports = {
'Display Error Message For Invalid GitHub URL Modal': function (browser: NightwatchBrowser) { 'Display Error Message For Invalid GitHub URL Modal': function (browser: NightwatchBrowser) {
browser browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]') .scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]') .waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]')
.execute(() => { .execute(() => {
@ -48,8 +46,6 @@ module.exports = {
'Import From Github For Valid URL': function (browser: NightwatchBrowser) { 'Import From Github For Valid URL': function (browser: NightwatchBrowser) {
browser browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]') .scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]') .waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]')
.clearValue('*[data-id="homeTabModalDialogCustomPromptText"]') .clearValue('*[data-id="homeTabModalDialogCustomPromptText"]')

@ -125,9 +125,9 @@ function startRemixd (browser: NightwatchBrowser) {
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.clickLaunchIcon('pluginManager') .clickLaunchIcon('pluginManager')
.scrollAndClick('#pluginManager *[data-id="pluginManagerComponentActivateButtonremixd"]') .scrollAndClick('#pluginManager *[data-id="pluginManagerComponentActivateButtonremixd"]')
.waitForElementVisible('#modal-footer-ok', 2000) .waitForElementVisible('*[data-id="remixdConnect-modal-footer-ok-react"]', 2000)
.pause(2000) .pause(2000)
.click('#modal-footer-ok') .click('*[data-id="remixdConnect-modal-footer-ok-react"]')
// .click('*[data-id="workspacesModalDialog-modal-footer-ok-react"]') // .click('*[data-id="workspacesModalDialog-modal-footer-ok-react"]')
} }

@ -18,7 +18,7 @@ declare module 'nightwatch' {
goToVMTraceStep(step: number, incr?: number): NightwatchBrowser, goToVMTraceStep(step: number, incr?: number): NightwatchBrowser,
checkVariableDebug(id: string, debugValue: NightwatchCheckVariableDebugValue): NightwatchBrowser, checkVariableDebug(id: string, debugValue: NightwatchCheckVariableDebugValue): NightwatchBrowser,
addAtAddressInstance(address: string, isValidFormat: boolean, isValidChecksum: boolean): NightwatchBrowser, addAtAddressInstance(address: string, isValidFormat: boolean, isValidChecksum: boolean): NightwatchBrowser,
modalFooterOKClick(): NightwatchBrowser, modalFooterOKClick(id?: string): NightwatchBrowser,
clickInstance(index: number): NightwatchBrowser, clickInstance(index: number): NightwatchBrowser,
journalLastChildIncludes(val: string): NightwatchBrowser, journalLastChildIncludes(val: string): NightwatchBrowser,
executeScript(script: string): NightwatchBrowser, executeScript(script: string): NightwatchBrowser,
@ -32,7 +32,7 @@ declare module 'nightwatch' {
scrollToLine(line: number): NightwatchBrowser, scrollToLine(line: number): NightwatchBrowser,
waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser, waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser,
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser, getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser,
modalFooterCancelClick(): NightwatchBrowser, modalFooterCancelClick(id?: string): NightwatchBrowser,
selectContract(contractName: string): NightwatchBrowser, selectContract(contractName: string): NightwatchBrowser,
createContract(inputParams: string): NightwatchBrowser, createContract(inputParams: string): NightwatchBrowser,
getAddressAtPosition(index: number, cb: (pos: string) => void): NightwatchBrowser, getAddressAtPosition(index: number, cb: (pos: string) => void): NightwatchBrowser,

@ -2,7 +2,6 @@
import { RunTab, makeUdapp } from './app/udapp' import { RunTab, makeUdapp } from './app/udapp'
import { RemixEngine } from './remixEngine' import { RemixEngine } from './remixEngine'
import { RemixAppManager } from './remixAppManager' import { RemixAppManager } from './remixAppManager'
import { MainView } from './app/panels/main-view'
import { ThemeModule } from './app/tabs/theme-module' import { ThemeModule } from './app/tabs/theme-module'
import { NetworkModule } from './app/tabs/network-module' import { NetworkModule } from './app/tabs/network-module'
import { Web3ProviderModule } from './app/tabs/web3-provider' import { Web3ProviderModule } from './app/tabs/web3-provider'
@ -11,15 +10,16 @@ import { HiddenPanel } from './app/components/hidden-panel'
import { VerticalIcons } from './app/components/vertical-icons' import { VerticalIcons } from './app/components/vertical-icons'
import { LandingPage } from './app/ui/landing-page/landing-page' import { LandingPage } from './app/ui/landing-page/landing-page'
import { MainPanel } from './app/components/main-panel' import { MainPanel } from './app/components/main-panel'
import { FramingService } from './framingService'
import { WalkthroughService } from './walkthroughService' import { WalkthroughService } from './walkthroughService'
import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, EditorContextListener } from '@remix-project/core-plugin' import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, EditorContextListener, GistHandler } from '@remix-project/core-plugin'
import migrateFileSystem from './migrateFileSystem' import migrateFileSystem from './migrateFileSystem'
import Registry from './app/state/registry' import Registry from './app/state/registry'
import { ConfigPlugin } from './app/plugins/config' import { ConfigPlugin } from './app/plugins/config'
import { Layout } from './app/panels/layout'
import { ModalPlugin } from './app/plugins/modal'
const isElectron = require('is-electron') const isElectron = require('is-electron')
@ -49,6 +49,7 @@ const TestTab = require('./app/tabs/test-tab')
const FilePanel = require('./app/panels/file-panel') const FilePanel = require('./app/panels/file-panel')
const Editor = require('./app/editor/editor') const Editor = require('./app/editor/editor')
const Terminal = require('./app/panels/terminal') const Terminal = require('./app/panels/terminal')
const { TabProxy } = require('./app/panels/tab-proxy.js')
class AppComponent { class AppComponent {
constructor () { constructor () {
@ -66,13 +67,27 @@ class AppComponent {
// load file system // load file system
self._components.filesProviders = {} self._components.filesProviders = {}
self._components.filesProviders.browser = new FileProvider('browser') self._components.filesProviders.browser = new FileProvider('browser')
Registry.getInstance().put({ api: self._components.filesProviders.browser, name: 'fileproviders/browser' }) Registry.getInstance().put({
self._components.filesProviders.localhost = new RemixDProvider(self.appManager) api: self._components.filesProviders.browser,
Registry.getInstance().put({ api: self._components.filesProviders.localhost, name: 'fileproviders/localhost' }) name: 'fileproviders/browser'
})
self._components.filesProviders.localhost = new RemixDProvider(
self.appManager
)
Registry.getInstance().put({
api: self._components.filesProviders.localhost,
name: 'fileproviders/localhost'
})
self._components.filesProviders.workspace = new WorkspaceFileProvider() self._components.filesProviders.workspace = new WorkspaceFileProvider()
Registry.getInstance().put({ api: self._components.filesProviders.workspace, name: 'fileproviders/workspace' }) Registry.getInstance().put({
api: self._components.filesProviders.workspace,
name: 'fileproviders/workspace'
})
Registry.getInstance().put({ api: self._components.filesProviders, name: 'fileproviders' }) Registry.getInstance().put({
api: self._components.filesProviders,
name: 'fileproviders'
})
migrateFileSystem(self._components.filesProviders.browser) migrateFileSystem(self._components.filesProviders.browser)
} }
@ -82,6 +97,7 @@ class AppComponent {
// APP_MANAGER // APP_MANAGER
const appManager = self.appManager const appManager = self.appManager
const pluginLoader = self.appManager.pluginLoader const pluginLoader = self.appManager.pluginLoader
self.panels = {}
self.workspace = pluginLoader.get() self.workspace = pluginLoader.get()
self.engine = new RemixEngine() self.engine = new RemixEngine()
self.engine.register(appManager) self.engine.register(appManager)
@ -91,8 +107,15 @@ class AppComponent {
'remix-beta.ethereum.org': 25, 'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23 'remix.ethereum.org': 23
} }
self.showMatamo = (matomoDomains[window.location.hostname] && !Registry.getInstance().get('config').api.exists('settings/matomo-analytics')) self.showMatamo =
self.walkthroughService = new WalkthroughService(appManager, self.showMatamo) matomoDomains[window.location.hostname] &&
!Registry.getInstance()
.get('config')
.api.exists('settings/matomo-analytics')
self.walkthroughService = new WalkthroughService(
appManager,
self.showMatamo
)
const hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080'] const hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080']
// workaround for Electron support // workaround for Electron support
@ -104,6 +127,8 @@ class AppComponent {
} }
// SERVICES // SERVICES
// ----------------- gist service ---------------------------------
self.gistHandler = new GistHandler()
// ----------------- theme service --------------------------------- // ----------------- theme service ---------------------------------
self.themeModule = new ThemeModule() self.themeModule = new ThemeModule()
Registry.getInstance().put({ api: self.themeModule, name: 'themeModule' }) Registry.getInstance().put({ api: self.themeModule, name: 'themeModule' })
@ -111,7 +136,9 @@ class AppComponent {
// ----------------- editor service ---------------------------- // ----------------- editor service ----------------------------
const editor = new Editor() // wrapper around ace editor const editor = new Editor() // wrapper around ace editor
Registry.getInstance().put({ api: editor, name: 'editor' }) Registry.getInstance().put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', () => fileManager.saveCurrentFile()) editor.event.register('requiringToSaveCurrentfile', () =>
fileManager.saveCurrentFile()
)
// ----------------- fileManager service ---------------------------- // ----------------- fileManager service ----------------------------
const fileManager = new FileManager(editor, appManager) const fileManager = new FileManager(editor, appManager)
@ -128,7 +155,10 @@ class AppComponent {
const compilerMetadataGenerator = new CompilerMetadata() const compilerMetadataGenerator = new CompilerMetadata()
// ----------------- compilation result service (can keep track of compilation results) ---------------------------- // ----------------- compilation result service (can keep track of compilation results) ----------------------------
const compilersArtefacts = new CompilerArtefacts() // store all the compilation results (key represent a compiler name) const compilersArtefacts = new CompilerArtefacts() // store all the compilation results (key represent a compiler name)
Registry.getInstance().put({ api: compilersArtefacts, name: 'compilersartefacts' }) Registry.getInstance().put({
api: compilersArtefacts,
name: 'compilersartefacts'
})
// service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it // service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it
const fetchAndCompile = new FetchAndCompile() const fetchAndCompile = new FetchAndCompile()
@ -139,28 +169,37 @@ class AppComponent {
const hardhatProvider = new HardhatProvider(blockchain) const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service ----------- // ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter() const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
Registry.getInstance().put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' }) Registry.getInstance().put({
api: offsetToLineColumnConverter,
name: 'offsettolinecolumnconverter'
})
// -------------------Terminal---------------------------------------- // -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl)) makeUdapp(blockchain, compilersArtefacts, domEl => terminal.logHtml(domEl))
const terminal = new Terminal( const terminal = new Terminal(
{ appManager, blockchain }, { appManager, blockchain },
{ {
getPosition: (event) => { getPosition: event => {
const limitUp = 36 const limitUp = 36
const limitDown = 20 const limitDown = 20
const height = window.innerHeight const height = window.innerHeight
let newpos = (event.pageY < limitUp) ? limitUp : event.pageY let newpos = event.pageY < limitUp ? limitUp : event.pageY
newpos = (newpos < height - limitDown) ? newpos : height - limitDown newpos = newpos < height - limitDown ? newpos : height - limitDown
return height - newpos return height - newpos
} }
} }
) )
const contextualListener = new EditorContextListener() const contextualListener = new EditorContextListener()
self.modal = new ModalPlugin()
const configPlugin = new ConfigPlugin() const configPlugin = new ConfigPlugin()
self.layout = new Layout()
self.engine.register([ self.engine.register([
self.layout,
self.modal,
self.gistHandler,
configPlugin, configPlugin,
blockchain, blockchain,
contentImport, contentImport,
@ -182,22 +221,27 @@ class AppComponent {
// LAYOUT & SYSTEM VIEWS // LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel() const appPanel = new MainPanel()
self.mainview = new MainView(contextualListener, editor, appPanel, fileManager, appManager, terminal)
Registry.getInstance().put({ api: self.mainview, name: 'mainview' }) Registry.getInstance().put({ api: self.mainview, name: 'mainview' })
const tabProxy = new TabProxy(fileManager, editor)
self.engine.register([ self.engine.register([appPanel, tabProxy])
appPanel,
self.mainview.tabProxy
])
// those views depend on app_manager // those views depend on app_manager
self.menuicons = new VerticalIcons(appManager) self.menuicons = new VerticalIcons(appManager)
self.sidePanel = new SidePanel(appManager, self.menuicons) self.sidePanel = new SidePanel(appManager, self.menuicons)
self.hiddenPanel = new HiddenPanel() self.hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(appManager, self.engine) const pluginManagerComponent = new PluginManagerComponent(
appManager,
self.engine
)
const filePanel = new FilePanel(appManager) const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(appManager, self.menuicons, fileManager, filePanel, contentImport) const landingPage = new LandingPage(
appManager,
self.menuicons,
fileManager,
filePanel,
contentImport
)
self.settings = new SettingsTab( self.settings = new SettingsTab(
Registry.getInstance().get('config').api, Registry.getInstance().get('config').api,
editor, editor,
@ -215,7 +259,10 @@ class AppComponent {
]) ])
// CONTENT VIEWS & DEFAULT PLUGINS // CONTENT VIEWS & DEFAULT PLUGINS
const compileTab = new CompileTab(Registry.getInstance().get('config').api, Registry.getInstance().get('filemanager').api) const compileTab = new CompileTab(
Registry.getInstance().get('config').api,
Registry.getInstance().get('filemanager').api
)
const run = new RunTab( const run = new RunTab(
blockchain, blockchain,
Registry.getInstance().get('config').api, Registry.getInstance().get('config').api,
@ -224,7 +271,6 @@ class AppComponent {
filePanel, filePanel,
Registry.getInstance().get('compilersartefacts').api, Registry.getInstance().get('compilersartefacts').api,
networkModule, networkModule,
self.mainview,
Registry.getInstance().get('fileproviders/browser').api Registry.getInstance().get('fileproviders/browser').api
) )
const analysis = new AnalysisTab() const analysis = new AnalysisTab()
@ -249,6 +295,13 @@ class AppComponent {
filePanel.hardhatHandle, filePanel.hardhatHandle,
filePanel.slitherHandle filePanel.slitherHandle
]) ])
self.layout.panels = {
tabs: { plugin: tabProxy, active: true },
editor: { plugin: editor, active: true },
main: { plugin: appPanel, active: false },
terminal: { plugin: terminal, active: true, minimized: false }
}
} }
async activate () { async activate () {
@ -263,61 +316,70 @@ class AppComponent {
try { try {
self.engine.register(await self.appManager.registeredPlugins()) self.engine.register(await self.appManager.registeredPlugins())
} catch (e) { } catch (e) {
console.log('couldn\'t register iframe plugins', e.message) console.log("couldn't register iframe plugins", e.message)
} }
await self.appManager.activatePlugin(['layout'])
await self.appManager.activatePlugin(['modal'])
await self.appManager.activatePlugin(['editor']) await self.appManager.activatePlugin(['editor'])
await self.appManager.activatePlugin(['theme', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter']) await self.appManager.activatePlugin(['theme', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await self.appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs']) await self.appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs'])
await self.appManager.activatePlugin(['sidePanel']) // activating host plugin separately await self.appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await self.appManager.activatePlugin(['home']) await self.appManager.activatePlugin(['home'])
await self.appManager.activatePlugin(['settings', 'config']) await self.appManager.activatePlugin(['settings', 'config'])
await self.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport']) await self.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await self.appManager.activatePlugin(['settings']) await self.appManager.activatePlugin(['settings'])
await self.appManager.activatePlugin(['walkthrough']) await self.appManager.activatePlugin(['walkthrough'])
self.appManager.on('filePanel', 'workspaceInitializationCompleted', async () => { self.appManager.on(
await self.appManager.registerContextMenuItems() 'filePanel',
}) 'workspaceInitializationCompleted',
async () => {
await self.appManager.registerContextMenuItems()
}
)
await self.appManager.activatePlugin(['filePanel']) await self.appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation // Set workspace after initial activation
self.appManager.on('editor', 'editorMounted', () => { self.appManager.on('editor', 'editorMounted', () => {
if (Array.isArray(self.workspace)) { if (Array.isArray(self.workspace)) {
self.appManager.activatePlugin(self.workspace).then(async () => { self.appManager
try { .activatePlugin(self.workspace)
if (params.deactivate) { .then(async () => {
await self.appManager.deactivatePlugin(params.deactivate.split(',')) try {
if (params.deactivate) {
await self.appManager.deactivatePlugin(
params.deactivate.split(',')
)
}
} catch (e) {
console.log(e)
}
if (params.code) {
// if code is given in url we focus on solidity plugin
self.menuicons.select('solidity')
} else {
// If plugins are loaded from the URL params, we focus on the last one.
if (
self.appManager.pluginLoader.current === 'queryParams' &&
self.workspace.length > 0
) { self.menuicons.select(self.workspace[self.workspace.length - 1]) }
} }
} catch (e) {
console.log(e) if (params.call) {
} const callDetails = params.call.split('//')
if (params.code) { if (callDetails.length > 1) {
// if code is given in url we focus on solidity plugin toolTip(`initiating ${callDetails[0]} ...`)
self.menuicons.select('solidity') // @todo(remove the timeout when activatePlugin is on 0.3.0)
} else { self.appManager.call(...callDetails).catch(console.error)
// If plugins are loaded from the URL params, we focus on the last one. }
if (self.appManager.pluginLoader.current === 'queryParams' && self.workspace.length > 0) self.menuicons.select(self.workspace[self.workspace.length - 1])
}
if (params.call) {
const callDetails = params.call.split('//')
if (callDetails.length > 1) {
toolTip(`initiating ${callDetails[0]} ...`)
// @todo(remove the timeout when activatePlugin is on 0.3.0)
self.appManager.call(...callDetails).catch(console.error)
} }
} })
}).catch(console.error) .catch(console.error)
} }
}) })
// activate solidity plugin // activate solidity plugin
self.appManager.activatePlugin(['solidity', 'udapp']) self.appManager.activatePlugin(['solidity', 'udapp'])
// Load and start the service who manager layout and frame // Load and start the service who manager layout and frame
const framingService = new FramingService(self.sidePanel, self.menuicons, self.mainview, null)
if (params.embed) framingService.embed()
framingService.start(params)
} }
} }

@ -1,31 +0,0 @@
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
const csjs = require('csjs-inject')
const yo = require('yo-yo')
const css = csjs`
.pluginsContainer {
display: none;
}
`
const profile = {
name: 'hiddenPanel',
displayName: 'Hidden Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
export class HiddenPanel extends AbstractPanel {
constructor () {
super(profile)
}
render () {
return yo`
<div class=${css.pluginsContainer}>
${this.view}
</div>`
}
}

@ -0,0 +1,37 @@
// eslint-disable-next-line no-use-before-define
import React from 'react'
import ReactDOM from 'react-dom' // eslint-disable-line
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
import { RemixPluginPanel } from '@remix-ui/panel'
const profile = {
name: 'hiddenPanel',
displayName: 'Hidden Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
export class HiddenPanel extends AbstractPanel {
el: HTMLElement
constructor () {
super(profile)
this.el = document.createElement('div')
this.el.setAttribute('class', 'pluginsContainer')
}
addView (profile: any, view: any): void {
super.removeView(profile)
super.addView(profile, view)
this.renderComponent()
}
render () {
return this.el
}
renderComponent () {
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.el)
}
}

@ -1,38 +0,0 @@
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const css = csjs`
.pluginsContainer {
height: 100%;
display: flex;
overflow-y: hidden;
}
`
const profile = {
name: 'mainPanel',
displayName: 'Main Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
export class MainPanel extends AbstractPanel {
constructor () {
super(profile)
}
focus (name) {
this.emit('focusChanged', name)
super.focus(name)
}
render () {
return yo`
<div class=${css.pluginsContainer} data-id="mainPanelPluginsContainer" id='mainPanelPluginsContainer-id'>
${this.view}
</div>`
}
}

@ -0,0 +1,57 @@
import React from 'react' // eslint-disable-line
import { AbstractPanel } from './panel'
import ReactDOM from 'react-dom' // eslint-disable-line
import { RemixPluginPanel } from '@remix-ui/panel'
import packageJson from '../../../../../package.json'
const profile = {
name: 'mainPanel',
displayName: 'Main Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView', 'showContent']
}
export class MainPanel extends AbstractPanel {
element: HTMLDivElement
constructor (config) {
super(profile)
this.element = document.createElement('div')
this.element.setAttribute('data-id', 'mainPanelPluginsContainer')
this.element.setAttribute('style', 'height: 100%; width: 100%;')
// this.config = config
}
onActivation () {
this.renderComponent()
}
focus (name) {
this.emit('focusChanged', name)
super.focus(name)
this.renderComponent()
}
addView (profile, view) {
super.addView(profile, view)
this.renderComponent()
}
removeView (profile) {
super.removeView(profile)
this.renderComponent()
}
async showContent (name) {
super.showContent(name)
this.renderComponent()
}
render () {
return this.element
}
renderComponent () {
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.element)
}
}

@ -1,111 +0,0 @@
import { EventEmitter } from 'events'
import { HostPlugin } from '@remixproject/engine-web'
const csjs = require('csjs-inject')
const yo = require('yo-yo')
const css = csjs`
.plugins {
height: 100%;
}
.plugItIn {
display : none;
height : 100%;
}
.plugItIn > div {
overflow-y : auto;
overflow-x : hidden;
height : 100%;
width : 100%;
}
.plugItIn.active {
display : block;
}
.pluginsContainer {
height : 100%;
overflow-y : hidden;
}
`
/** Abstract class used for hosting the view of a plugin */
export class AbstractPanel extends HostPlugin {
constructor (profile) {
super(profile)
this.events = new EventEmitter()
this.contents = {}
this.active = undefined
// View where the plugin HTMLElement leaves
this.view = yo`<div id="plugins" class="${css.plugins}"></div>`
}
/**
* Add the plugin to the panel
* @param {String} name the name of the plugin
* @param {HTMLElement} content the HTMLContent of the plugin
*/
add (view, name) {
if (this.contents[name]) throw new Error(`Plugin ${name} already rendered`)
view.style.height = '100%'
view.style.width = '100%'
view.style.border = '0'
const isIframe = view.tagName === 'IFRAME'
view.style.display = isIframe ? 'none' : 'block'
const loading = isIframe ? yo`
<div class="d-flex justify-content-center align-items-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
` : ''
this.contents[name] = yo`<div class="${css.plugItIn}" >${view}${loading}</div>`
if (view.tagName === 'IFRAME') {
view.addEventListener('load', () => {
if (this.contents[name].contains(loading)) {
this.contents[name].removeChild(loading)
}
view.style.display = 'block'
})
}
this.contents[name].style.display = 'none'
this.view.appendChild(this.contents[name])
}
addView (profile, view) {
this.add(view, profile.name)
}
removeView (profile) {
this.remove(profile.name)
}
/**
* Remove a plugin from the panel
* @param {String} name The name of the plugin to remove
*/
remove (name) {
const el = this.contents[name]
delete this.contents[name]
if (el) el.parentElement.removeChild(el)
if (name === this.active) this.active = undefined
}
/**
* Display the content of this specific plugin
* @param {String} name The name of the plugin to display the content
*/
showContent (name) {
if (!this.contents[name]) throw new Error(`Plugin ${name} is not yet activated`)
// hiding the current view and display the `moduleName`
if (this.active) {
this.contents[this.active].style.display = 'none'
}
this.contents[name].style.display = 'flex'
this.active = name
}
focus (name) {
this.showContent(name)
}
}

@ -0,0 +1,63 @@
import React from 'react' // eslint-disable-line
import { EventEmitter } from 'events'
import { HostPlugin } from '@remixproject/engine-web' // eslint-disable-line
import { PluginRecord } from 'libs/remix-ui/panel/src/lib/types'
const EventManager = require('../../lib/events')
export class AbstractPanel extends HostPlugin {
events: EventEmitter
event: any
public plugins: Record<string, PluginRecord> = {}
constructor (profile) {
super(profile)
this.events = new EventEmitter()
this.event = new EventManager()
}
currentFocus (): string {
return Object.values(this.plugins).find(plugin => {
return plugin.active
}).profile.name
}
addView (profile, view) {
if (this.plugins[profile.name]) throw new Error(`Plugin ${profile.name} already rendered`)
this.plugins[profile.name] = {
profile: profile,
view: view,
active: false,
class: 'plugItIn active'
}
}
removeView (profile) {
this.emit('pluginDisabled', profile.name)
this.call('menuicons', 'unlinkContent', profile)
this.remove(profile.name)
}
/**
* Remove a plugin from the panel
* @param {String} name The name of the plugin to remove
*/
remove (name) {
delete this.plugins[name]
}
/**
* Display the content of this specific plugin
* @param {String} name The name of the plugin to display the content
*/
showContent (name) {
if (!this.plugins[name]) throw new Error(`Plugin ${name} is not yet activated`)
Object.values(this.plugins).forEach(plugin => {
plugin.active = false
})
this.plugins[name].active = true
}
focus (name) {
this.showContent(name)
}
}

@ -1,156 +0,0 @@
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
const csjs = require('csjs-inject')
const yo = require('yo-yo')
const css = csjs`
.panel {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex: auto;
}
.swapitTitle {
margin: 0;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.swapitTitle i{
padding-left: 6px;
font-size: 14px;
}
.swapitHeader {
display: flex;
align-items: center;
padding: 16px 24px 15px;
justify-content: space-between;
}
.icons i {
height: 80%;
cursor: pointer;
}
.pluginsContainer {
height: 100%;
overflow-y: auto;
}
.titleInfo {
padding-left: 10px;
}
.versionBadge {
background-color: var(--light);
padding: 0 7px;
font-weight: bolder;
margin-left: 5px;
text-transform: lowercase;
cursor: default;
}
`
const sidePanel = {
name: 'sidePanel',
displayName: 'Side Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
// TODO merge with vertical-icons.js
export class SidePanel extends AbstractPanel {
constructor (appManager, verticalIcons) {
super(sidePanel)
this.appManager = appManager
this.header = yo`<header></header>`
this.renderHeader()
this.verticalIcons = verticalIcons
// Toggle content
verticalIcons.events.on('toggleContent', (name) => {
if (!this.contents[name]) return
if (this.active === name) {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('toggle', name)
this.events.emit('toggle', name)
return
}
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
// Force opening
verticalIcons.events.on('showContent', (name) => {
if (!this.contents[name]) return
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
}
focus (name) {
this.emit('focusChanged', name)
super.focus(name)
}
removeView (profile) {
super.removeView(profile)
this.emit('pluginDisabled', profile.name)
this.verticalIcons.unlinkContent(profile)
}
addView (profile, view) {
super.addView(profile, view)
this.verticalIcons.linkContent(profile)
}
/**
* Display content and update the header
* @param {String} name The name of the plugin to display
*/
async showContent (name) {
super.showContent(name)
this.renderHeader()
this.emit('focusChanged', name)
}
/** The header of the side panel */
async renderHeader () {
let name = ' - '
let docLink = ''
let versionWarning
if (this.active) {
const profile = await this.appManager.getProfile(this.active)
name = profile.displayName ? profile.displayName : profile.name
docLink = profile.documentation ? yo`<a href="${profile.documentation}" class="${css.titleInfo}" title="link to documentation" target="_blank"><i aria-hidden="true" class="fas fa-book"></i></a>` : ''
if (profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)) {
versionWarning = yo`<small title="Version Alpha" class="badge-light ${css.versionBadge}">alpha</small>`
}
// Beta
if (profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)) {
versionWarning = yo`<small title="Version Beta" class="badge-light ${css.versionBadge}">beta</small>`
}
}
const header = yo`
<header class="${css.swapitHeader}">
<h6 class="${css.swapitTitle}" data-id="sidePanelSwapitTitle">${name}</h6>
${docLink}
${versionWarning}
</header>
`
yo.update(this.header, header)
}
render () {
return yo`
<section class="${css.panel} plugin-manager">
${this.header}
<div class="${css.pluginsContainer}">
${this.view}
</div>
</section>`
}
}

@ -0,0 +1,95 @@
// eslint-disable-next-line no-use-before-define
import React from 'react'
import ReactDOM from 'react-dom'
import { AbstractPanel } from './panel'
import { RemixPluginPanel } from '@remix-ui/panel'
import packageJson from '../../../../../package.json'
import { RemixAppManager } from '../../remixAppManager'
import { VerticalIcons } from 'libs/remix-ui/vertical-icons-panel/types/vertical-icons-panel'
import RemixUIPanelHeader from 'libs/remix-ui/panel/src/lib/plugins/panel-header'
// const csjs = require('csjs-inject')
const sidePanel = {
name: 'sidePanel',
displayName: 'Side Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
// TODO merge with vertical-icons.js
export class SidePanel extends AbstractPanel {
appManager: RemixAppManager
sideelement: any
verticalIcons: VerticalIcons;
constructor (appManager: RemixAppManager, verticalIcons: VerticalIcons) {
super(sidePanel)
this.appManager = appManager
this.sideelement = document.createElement('section')
this.sideelement.setAttribute('class', 'panel plugin-manager')
this.verticalIcons = verticalIcons
// Toggle content
verticalIcons.events.on('toggleContent', (name) => {
if (!this.plugins[name]) return
if (this.plugins[name].active) {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('toggle', name)
this.events.emit('toggle', name)
return
}
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
// Force opening
verticalIcons.events.on('showContent', (name) => {
if (!this.plugins[name]) return
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
}
onActivation () {
this.renderComponent()
}
focus (name) {
this.emit('focusChanged', name)
super.focus(name)
}
removeView (profile) {
super.removeView(profile)
this.emit('pluginDisabled', profile.name)
this.call('menuicons', 'unlinkContent', profile)
this.renderComponent()
}
addView (profile, view) {
super.addView(profile, view)
this.verticalIcons.linkContent(profile)
this.renderComponent()
}
/**
* Display content and update the header
* @param {String} name The name of the plugin to display
*/
async showContent (name) {
super.showContent(name)
this.emit('focusChanged', name)
this.renderComponent()
}
render () {
return this.sideelement
}
renderComponent () {
ReactDOM.render(<RemixPluginPanel header={<RemixUIPanelHeader plugins={this.plugins}></RemixUIPanelHeader>} plugins={this.plugins}/>, this.sideelement)
}
}

@ -15,7 +15,7 @@ const profile = {
displayName: 'Vertical Icons', displayName: 'Vertical Icons',
description: '', description: '',
version: packageJson.version, version: packageJson.version,
methods: ['select'] methods: ['select', 'unlinkContent']
} }
// TODO merge with side-panel.js. VerticalIcons should not be a plugin // TODO merge with side-panel.js. VerticalIcons should not be a plugin

@ -5,9 +5,9 @@ import async from 'async'
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import Registry from '../state/registry' import Registry from '../state/registry'
const EventEmitter = require('events') import { EventEmitter } from 'events'
import { RemixAppManager } from '../../../../../libs/remix-ui/plugin-manager/src/types'
const toaster = require('../ui/tooltip') const toaster = require('../ui/tooltip')
const modalDialogCustom = require('../ui/modal-dialog-custom')
const helper = require('../../lib/helper.js') const helper = require('../../lib/helper.js')
/* /*
@ -22,7 +22,7 @@ const profile = {
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
permission: true, permission: true,
version: packageJson.version, version: packageJson.version,
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile'], methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'],
kind: 'file-system' kind: 'file-system'
} }
const errorMsg = { const errorMsg = {
@ -37,6 +37,18 @@ const createError = (err) => {
} }
class FileManager extends Plugin { class FileManager extends Plugin {
mode: string
openedFiles: any
events: EventEmitter
editor: any
_components: any
appManager: RemixAppManager
_deps: any
getCurrentFile: () => any
getFile: (path: any) => Promise<unknown>
getFolder: (path: any) => Promise<unknown>
setFile: (path: any, data: any) => Promise<unknown>
switchFile: (path: any) => Promise<void>
constructor (editor, appManager) { constructor (editor, appManager) {
super(profile) super(profile)
this.mode = 'browser' this.mode = 'browser'
@ -70,7 +82,7 @@ class FileManager extends Plugin {
* @param {string} path path of the file/directory * @param {string} path path of the file/directory
* @param {string} message message to display if path doesn't exist. * @param {string} message message to display if path doesn't exist.
*/ */
async _handleExists (path, message) { async _handleExists (path: string, message?:string) {
const exists = await this.exists(path) const exists = await this.exists(path)
if (!exists) { if (!exists) {
@ -96,7 +108,7 @@ class FileManager extends Plugin {
* @param {string} path path of the file/directory * @param {string} path path of the file/directory
* @param {string} message message to display if path is not a directory. * @param {string} message message to display if path is not a directory.
*/ */
async _handleIsDir (path, message) { async _handleIsDir (path: string, message?: string) {
const isDir = await this.isDirectory(path) const isDir = await this.isDirectory(path)
if (!isDir) { if (!isDir) {
@ -305,13 +317,19 @@ class FileManager extends Plugin {
if (isFile) { if (isFile) {
if (newPathExists) { if (newPathExists) {
modalDialogCustom.alert('File already exists.') this.call('modal', 'alert', {
id: 'fileManagerAlert',
message: 'File already exists'
})
return return
} }
return provider.rename(oldPath, newPath, false) return provider.rename(oldPath, newPath, false)
} else { } else {
if (newPathExists) { if (newPathExists) {
modalDialogCustom.alert('Folder already exists.') this.call('modal', 'alert', {
id: 'fileManagerAlert',
message: 'Directory already exists'
})
return return
} }
return provider.rename(oldPath, newPath, true) return provider.rename(oldPath, newPath, true)
@ -612,7 +630,7 @@ class FileManager extends Plugin {
this.events.emit('noFileSelected') this.events.emit('noFileSelected')
} }
async openFile (file) { async openFile (file?: string) {
if (!file) { if (!file) {
this.emit('noFileSelected') this.emit('noFileSelected')
this.events.emit('noFileSelected') this.events.emit('noFileSelected')
@ -639,7 +657,7 @@ class FileManager extends Plugin {
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('currentFileChanged', file) this.emit('currentFileChanged', file)
this.events.emit('currentFileChanged', file) this.events.emit('currentFileChanged', file)
resolve() resolve(true)
} }
}) })
}) })
@ -698,7 +716,7 @@ class FileManager extends Plugin {
dirPaths.push(item) dirPaths.push(item)
resolve(dirPaths) resolve(dirPaths)
} }
return new Promise((resolve, reject) => { resolve() }) return new Promise((resolve, reject) => { resolve(true) })
}) })
Promise.all(promises).then(() => { resolve(dirPaths) }) Promise.all(promises).then(() => { resolve(dirPaths) })
}) })
@ -760,9 +778,15 @@ class FileManager extends Plugin {
helper.createNonClashingName(file, self._deps.filesProviders[fileProvider], helper.createNonClashingName(file, self._deps.filesProviders[fileProvider],
(error, name) => { (error, name) => {
if (error) { if (error) {
modalDialogCustom.alert('Unexpected error loading the file ' + error) this.call('modal', 'alert', {
id: 'fileManagerAlert',
message: 'Unexpected error loading file ' + file + ': ' + error
})
} else if (helper.checkSpecialChars(name)) { } else if (helper.checkSpecialChars(name)) {
modalDialogCustom.alert('Special characters are not allowed') this.call('modal', 'alert', {
id: 'fileManagerAlert',
message: 'Special characters are not allowed in file names.'
})
} else { } else {
try { try {
self._deps.filesProviders[fileProvider].set(name, filesSet[file].content) self._deps.filesProviders[fileProvider].set(name, filesSet[file].content)

@ -5,7 +5,7 @@ import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
import Registry from '../state/registry' import Registry from '../state/registry'
const { RemixdHandle } = require('../files/remixd-handle.js') import { RemixdHandle } from '../plugins/remixd-handle'
const { GitHandle } = require('../files/git-handle.js') const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js') const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js') const { SlitherHandle } = require('../files/slither-handle.js')

@ -0,0 +1,94 @@
import { Plugin } from '@remixproject/engine'
import { Profile } from '@remixproject/plugin-utils'
import { EventEmitter } from 'events'
import QueryParams from '../../lib/query-params'
const profile: Profile = {
name: 'layout',
description: 'layout',
methods: ['minimize']
}
interface panelState {
active: boolean
plugin: Plugin
minimized: boolean
}
interface panels {
tabs: panelState
editor: panelState
main: panelState
terminal: panelState
}
export class Layout extends Plugin {
event: any
panels: panels
constructor () {
super(profile)
this.event = new EventEmitter()
}
async onActivation (): Promise<void> {
this.on('fileManager', 'currentFileChanged', () => {
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'openFile', () => {
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'switchApp', (name: string) => {
this.call('mainPanel', 'showContent', name)
this.panels.editor.active = false
this.panels.main.active = true
this.event.emit('change', null)
})
this.on('tabs', 'closeApp', (name: string) => {
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'tabCountChanged', async count => {
if (!count) await this.call('manager', 'activatePlugin', 'home')
})
this.on('manager', 'activate', (profile: Profile) => {
switch (profile.name) {
case 'filePanel':
this.call('menuicons', 'select', 'filePanel')
break
}
})
document.addEventListener('keypress', e => {
if (e.shiftKey && e.ctrlKey) {
if (e.code === 'KeyF') {
// Ctrl+Shift+F
this.call('menuicons', 'select', 'filePanel')
} else if (e.code === 'KeyA') {
// Ctrl+Shift+A
this.call('menuicons', 'select', 'pluginManager')
} else if (e.code === 'KeyS') {
// Ctrl+Shift+S
this.call('menuicons', 'select', 'settings')
}
e.preventDefault()
}
})
const queryParams = new QueryParams()
const params = queryParams.get()
if (params.minimizeterminal || params.embed) {
this.panels.terminal.minimized = true
this.event.emit('change', null)
}
if (params.minimizesidepanel || params.embed) {
this.event.emit('minimizesidepanel')
}
}
minimize (name: string, minimized:boolean): void {
this.panels[name].minimized = minimized
this.event.emit('change', null)
}
}

@ -1,204 +0,0 @@
import Registry from '../state/registry'
var yo = require('yo-yo')
var EventManager = require('../../lib/events')
var { TabProxy } = require('./tab-proxy.js')
var csjs = require('csjs-inject')
var css = csjs`
.mainview {
display : flex;
flex-direction : column;
height : 100%;
width : 100%;
}
`
// @todo(#650) Extract this into two classes: MainPanel (TabsProxy + Iframe/Editor) & BottomPanel (Terminal)
export class MainView {
constructor (contextualListener, editor, mainPanel, fileManager, appManager, terminal) {
var self = this
self.event = new EventManager()
self._view = {}
self._components = {}
self._components.registry = Registry.getInstance()
self.contextualListener = contextualListener
self.editor = editor
self.fileManager = fileManager
self.mainPanel = mainPanel
self.txListener = Registry.getInstance().get('txlistener').api
self._components.terminal = terminal
this.appManager = appManager
this.init()
}
showApp (name) {
this.fileManager.unselectCurrentFile()
this.mainPanel.showContent(name)
this._view.editor.style.display = 'none'
this._view.mainPanel.style.display = 'block'
}
getAppPanel () {
return this.mainPanel
}
init () {
var self = this
self._deps = {
config: self._components.registry.get('config').api,
fileManager: self._components.registry.get('filemanager').api
}
self.tabProxy = new TabProxy(self.fileManager, self.editor)
/*
We listen here on event from the tab component to display / hide the editor and mainpanel
depending on the content that should be displayed
*/
self.fileManager.events.on('currentFileChanged', (file) => {
// we check upstream for "fileChanged"
self._view.editor.style.display = 'block'
self._view.mainPanel.style.display = 'none'
})
self.tabProxy.event.on('openFile', (file) => {
self._view.editor.style.display = 'block'
self._view.mainPanel.style.display = 'none'
})
self.tabProxy.event.on('closeFile', (file) => {
})
self.tabProxy.event.on('switchApp', self.showApp.bind(self))
self.tabProxy.event.on('closeApp', (name) => {
self._view.editor.style.display = 'block'
self._view.mainPanel.style.display = 'none'
})
self.tabProxy.event.on('tabCountChanged', (count) => {
if (!count) this.editor.displayEmptyReadOnlySession()
})
self.data = {
_layout: {
top: {
offset: self._terminalTopOffset(),
show: true
}
}
}
self._components.terminal.event.register('resize', delta => self._adjustLayout('top', delta))
if (self.txListener) {
self._components.terminal.event.register('listenOnNetWork', (listenOnNetWork) => {
self.txListener.setListenOnNetwork(listenOnNetWork)
})
}
}
_terminalTopOffset () {
return this._deps.config.get('terminal-top-offset') || 150
}
_adjustLayout (direction, delta) {
var limitUp = 0
var limitDown = 32
var containerHeight = window.innerHeight - limitUp // - menu bar containerHeight
var self = this
var layout = self.data._layout[direction]
if (layout) {
if (delta === undefined) {
layout.show = !layout.show
if (layout.show) delta = layout.offset
else delta = 0
} else {
layout.show = true
self._deps.config.set(`terminal-${direction}-offset`, delta)
layout.offset = delta
}
}
var tmp = delta - limitDown
delta = tmp > 0 ? tmp : 0
if (direction === 'top') {
var mainPanelHeight = containerHeight - delta
mainPanelHeight = mainPanelHeight < 0 ? 0 : mainPanelHeight
self._view.editor.style.height = `${mainPanelHeight}px`
self._view.mainPanel.style.height = `${mainPanelHeight}px`
self._view.terminal.style.height = `${delta}px` // - menu bar height
self.editor.resize((document.querySelector('#editorWrap') || {}).checked)
self._components.terminal.scroll2bottom()
}
}
minimizeTerminal () {
this._adjustLayout('top')
}
showTerminal (offset) {
this._adjustLayout('top', offset || this._terminalTopOffset())
}
getTerminal () {
return this._components.terminal
}
getEditor () {
var self = this
return self.editor
}
refresh () {
var self = this
self._view.tabs.onmouseenter()
}
log (data = {}) {
var self = this
var command = self._components.terminal.commands[data.type]
if (typeof command === 'function') command(data.value)
}
logMessage (msg) {
var self = this
self.log({ type: 'log', value: msg })
}
logHtmlMessage (msg) {
var self = this
self.log({ type: 'html', value: msg })
}
render () {
var self = this
if (self._view.mainview) return self._view.mainview
self._view.editor = self.editor.render()
self._view.editor.style.display = 'none'
self._view.mainPanel = self.mainPanel.render()
self._view.terminal = self._components.terminal.render()
self._view.mainview = yo`
<div class=${css.mainview}>
${self.tabProxy.renderTabsbar()}
${self._view.editor}
${self._view.mainPanel}
<div class="${css.contextview} contextview"></div>
${self._view.terminal}
</div>
`
// INIT
self._adjustLayout('top', self.data._layout.top.offset)
document.addEventListener('keydown', (e) => {
if (e.altKey && e.keyCode === 84) self.tabProxy.switchNextTab() // alt + t
})
return self._view.mainview
}
registerCommand (name, command, opts) {
var self = this
return self._components.terminal.registerCommand(name, command, opts)
}
updateTerminalFilter (filter) {
this._components.terminal.updateJournal(filter)
}
}

@ -22,6 +22,7 @@ export class TabProxy extends Plugin {
this._view = {} this._view = {}
this._handlers = {} this._handlers = {}
this.loadedTabs = [] this.loadedTabs = []
this.el = document.createElement('div')
} }
onActivation () { onActivation () {
@ -72,10 +73,12 @@ export class TabProxy extends Plugin {
this.addTab(workspacePath, '', () => { this.addTab(workspacePath, '', () => {
this.fileManager.open(file) this.fileManager.open(file)
this.event.emit('openFile', file) this.event.emit('openFile', file)
this.emit('openFile', file)
}, },
() => { () => {
this.fileManager.closeFile(file) this.fileManager.closeFile(file)
this.event.emit('closeFile', file) this.event.emit('closeFile', file)
this.emit('closeFile', file)
}) })
this.tabsApi.activateTab(workspacePath) this.tabsApi.activateTab(workspacePath)
} else { } else {
@ -88,10 +91,12 @@ export class TabProxy extends Plugin {
this.addTab(path, '', () => { this.addTab(path, '', () => {
this.fileManager.open(file) this.fileManager.open(file)
this.event.emit('openFile', file) this.event.emit('openFile', file)
this.emit('openFile', file)
}, },
() => { () => {
this.fileManager.closeFile(file) this.fileManager.closeFile(file)
this.event.emit('closeFile', file) this.event.emit('closeFile', file)
this.emit('closeFile', file)
}) })
this.tabsApi.activateTab(path) this.tabsApi.activateTab(path)
} }
@ -132,9 +137,9 @@ export class TabProxy extends Plugin {
this.addTab( this.addTab(
name, name,
displayName, displayName,
() => this.event.emit('switchApp', name), () => this.emit('switchApp', name),
() => { () => {
this.event.emit('closeApp', name) this.emit('closeApp', name)
this.call('manager', 'deactivatePlugin', name) this.call('manager', 'deactivatePlugin', name)
}, },
icon icon
@ -149,7 +154,7 @@ export class TabProxy extends Plugin {
} }
focus (name) { focus (name) {
this.event.emit('switchApp', name) this.emit('switchApp', name)
this.tabsApi.activateTab(name) this.tabsApi.activateTab(name)
} }
@ -199,6 +204,7 @@ export class TabProxy extends Plugin {
() => { () => {
this.fileManager.closeFile(newName) this.fileManager.closeFile(newName)
this.event.emit('closeFile', newName) this.event.emit('closeFile', newName)
this.emit('closeFile', newName)
}) })
this.removeTab(oldName) this.removeTab(oldName)
} }
@ -285,7 +291,7 @@ export class TabProxy extends Plugin {
if (this.loadedTabs[index]) { if (this.loadedTabs[index]) {
const name = this.loadedTabs[index].name const name = this.loadedTabs[index].name
if (this._handlers[name]) this._handlers[name].switchTo() if (this._handlers[name]) this._handlers[name].switchTo()
this.event.emit('tabCountChanged', this.loadedTabs.length) this.emit('tabCountChanged', this.loadedTabs.length)
} }
} }
@ -293,7 +299,7 @@ export class TabProxy extends Plugin {
if (this.loadedTabs[index]) { if (this.loadedTabs[index]) {
const name = this.loadedTabs[index].name const name = this.loadedTabs[index].name
if (this._handlers[name]) this._handlers[name].close() if (this._handlers[name]) this._handlers[name].close()
this.event.emit('tabCountChanged', this.loadedTabs.length) this.emit('tabCountChanged', this.loadedTabs.length)
} }
} }
@ -308,8 +314,6 @@ export class TabProxy extends Plugin {
} }
renderTabsbar () { renderTabsbar () {
this.el = document.createElement('div')
this.renderComponent()
return this.el return this.el
} }
} }

@ -13,8 +13,6 @@ const AutoCompletePopup = require('../ui/auto-complete-popup')
import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line
const GistHandler = require('../../lib/gist-handler')
const KONSOLES = [] const KONSOLES = []
function register (api) { KONSOLES.push(api) } function register (api) { KONSOLES.push(api) }
@ -22,7 +20,7 @@ function register (api) { KONSOLES.push(api) }
const profile = { const profile = {
displayName: 'Terminal', displayName: 'Terminal',
name: 'terminal', name: 'terminal',
methods: ['log'], methods: ['log', 'logHtml'],
events: [], events: [],
description: ' - ', description: ' - ',
version: packageJson.version version: packageJson.version
@ -32,7 +30,6 @@ class Terminal extends Plugin {
constructor (opts, api) { constructor (opts, api) {
super(profile) super(profile)
this.fileImport = new CompilerImports() this.fileImport = new CompilerImports()
this.gistHandler = new GistHandler()
this.event = new EventManager() this.event = new EventManager()
this.globalRegistry = Registry.getInstance() this.globalRegistry = Registry.getInstance()
this.element = document.createElement('div') this.element = document.createElement('div')

@ -0,0 +1,44 @@
import { Plugin } from '@remixproject/engine'
import { LibraryProfile, MethodApi, StatusEvents } from '@remixproject/plugin-utils'
import { AppModal } from '@remix-ui/app'
import { AlertModal } from 'libs/remix-ui/app/src/lib/remix-app/interface'
import { dispatchModalInterface } from 'libs/remix-ui/app/src/lib/remix-app/context/context'
interface IModalApi {
events: StatusEvents,
methods: {
modal: (args: AppModal) => void
alert: (args: AlertModal) => void
toast: (message: string) => void
}
}
const profile:LibraryProfile<IModalApi> = {
name: 'modal',
displayName: 'Modal',
description: 'Modal',
methods: ['modal', 'alert', 'toast']
}
export class ModalPlugin extends Plugin implements MethodApi<IModalApi> {
dispatcher: dispatchModalInterface
constructor () {
super(profile)
}
setDispatcher (dispatcher: dispatchModalInterface) {
this.dispatcher = dispatcher
}
async modal (args: AppModal) {
this.dispatcher.modal(args)
}
async alert (args: AlertModal) {
this.dispatcher.alert(args)
}
async toast (message: string) {
this.dispatcher.toast(message)
}
}

@ -1,24 +1,13 @@
/* eslint-disable no-unused-vars */
import React, { useRef, useState, useEffect } from 'react' // eslint-disable-line
import isElectron from 'is-electron' import isElectron from 'is-electron'
import { WebsocketPlugin } from '@remixproject/engine-web' import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import { version as remixdVersion } from '../../../../../libs/remixd/package.json' import { version as remixdVersion } from '../../../../../libs/remixd/package.json'
var yo = require('yo-yo') import { PluginManager } from '@remixproject/engine'
var modalDialog = require('../ui/modaldialog') import { AppModal, AlertModal } from '@remix-ui/app'
var modalDialogCustom = require('../ui/modal-dialog-custom') import { CopyToClipboard } from '@remix-ui/clipboard'
var copyToClipboard = require('../ui/copy-to-clipboard')
var csjs = require('csjs-inject')
var css = csjs`
.dialog {
display: flex;
flex-direction: column;
}
.dialogParagraph {
margin-bottom: 2em;
word-break: break-word;
}
`
const LOCALHOST = ' - connect to localhost - ' const LOCALHOST = ' - connect to localhost - '
const profile = { const profile = {
@ -32,29 +21,37 @@ const profile = {
version: packageJson.version version: packageJson.version
} }
enum State {
ok,
cancel,
new
}
export class RemixdHandle extends WebsocketPlugin { export class RemixdHandle extends WebsocketPlugin {
localhostProvider: any
appManager: PluginManager
state: State
constructor (localhostProvider, appManager) { constructor (localhostProvider, appManager) {
super(profile) super(profile)
this.localhostProvider = localhostProvider this.localhostProvider = localhostProvider
this.appManager = appManager this.appManager = appManager
} }
deactivate () { async deactivate () {
if (super.socket) super.deactivate() if (super.socket) super.deactivate()
// this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342 // this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat') if (this.appManager.isActive('hardhat')) this.appManager.deactivatePlugin('hardhat')
if (this.appManager.actives.includes('slither')) this.appManager.deactivatePlugin('slither') if (this.appManager.isActive('slither')) this.appManager.deactivatePlugin('slither')
this.localhostProvider.close((error) => { this.localhostProvider.close((error) => {
if (error) console.log(error) if (error) console.log(error)
}) })
} }
activate () { async activate () {
this.connectToLocalhost() await this.connectToLocalhost()
} }
async canceled () { async canceled () {
// await this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
await this.appManager.deactivatePlugin('remixd') await this.appManager.deactivatePlugin('remixd')
} }
@ -65,23 +62,25 @@ export class RemixdHandle extends WebsocketPlugin {
* @param {String} txHash - hash of the transaction * @param {String} txHash - hash of the transaction
*/ */
async connectToLocalhost () { async connectToLocalhost () {
const connection = (error) => { const connection = (error?:any) => {
if (error) { if (error) {
console.log(error) console.log(error)
modalDialogCustom.alert( const alert:AlertModal = {
'Cannot connect to the remixd daemon. ' + id: 'connectionAlert',
'Please make sure you have the remixd running in the background.' message: 'Cannot connect to the remixd daemon. Please make sure you have the remixd running in the background.'
) }
this.call('modal', 'alert', alert)
this.canceled() this.canceled()
} else { } else {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
clearInterval(intervalId) clearInterval(intervalId)
console.log(error) console.log(error)
modalDialogCustom.alert( const alert:AlertModal = {
'Connection to remixd terminated. ' + id: 'connectionAlert',
'Please make sure remixd is still running in the background.' message: 'Connection to remixd terminated.Please make sure remixd is still running in the background.'
) }
this.call('modal', 'alert', alert)
this.canceled() this.canceled()
} }
}, 3000) }, 3000)
@ -96,34 +95,38 @@ export class RemixdHandle extends WebsocketPlugin {
this.deactivate() this.deactivate()
} else if (!isElectron()) { } else if (!isElectron()) {
// warn the user only if he/she is in the browser context // warn the user only if he/she is in the browser context
modalDialog( this.state = State.new
'Connect to localhost', const mod:AppModal = {
remixdDialog(), id: 'remixdConnect',
{ title: 'Connect to localhost',
label: 'Connect', message: remixdDialog(),
fn: () => { okFn: () => {
try { this.state = State.ok
this.localhostProvider.preInit() try {
super.activate() this.localhostProvider.preInit()
setTimeout(() => { super.activate()
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed setTimeout(() => {
connection(new Error('Connection with daemon failed.')) if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
} else { connection(new Error('Connection with daemon failed.'))
connection() } else {
} connection()
}, 3000) }
} catch (error) { }, 3000)
connection(error) } catch (error) {
} connection(error)
} }
}, },
{ cancelFn: async () => {
label: 'Cancel', this.state = State.cancel
fn: () => { await this.canceled()
this.canceled() },
} okLabel: 'Connect',
cancelLabel: 'Cancel',
hideFn: async () => {
if (this.state === State.new) await this.canceled()
} }
) }
await this.call('modal', 'modal', mod)
} else { } else {
try { try {
super.activate() super.activate()
@ -137,31 +140,31 @@ export class RemixdHandle extends WebsocketPlugin {
function remixdDialog () { function remixdDialog () {
const commandText = 'remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>' const commandText = 'remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>'
return yo` return (<>
<div class=${css.dialog}> <div className=''>
<div class=${css.dialogParagraph}> <div className='mb-2 text-break'>
Access your local file system from Remix IDE using <a target="_blank" href="https://www.npmjs.com/package/@remix-project/remixd">Remixd NPM package</a>.<br/><br/> Access your local file system from Remix IDE using <a target="_blank" href="https://www.npmjs.com/package/@remix-project/remixd">Remixd NPM package</a>.<br/><br/>
Remixd needs to be running in the background to load the files in localhost workspace. For more info, please check the <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html">Remixd tutorial</a>. Remixd needs to be running in the background to load the files in localhost workspace. For more info, please check the <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html">Remixd tutorial</a>.
</div> </div>
<div class=${css.dialogParagraph}> <div className='mb-2 text-break'>
If you are just looking for the remixd command, here it is: If you are just looking for the remixd command, here it is:
<br><br><b>${commandText}</b> <br></br><br></br><b>${commandText}</b>
<span class="">${copyToClipboard(() => commandText)}</span> <CopyToClipboard data-id='remixdCopyCommand' content={commandText}></CopyToClipboard>
</div> </div>
<div class=${css.dialogParagraph}> <div className='mb-2 text-break'>
When connected, a session will be started between <em>${window.location.origin}</em> and your local file system at <i>ws://127.0.0.1:65520</i>. When connected, a session will be started between <em>${window.location.origin}</em> and your local file system at <i>ws://127.0.0.1:65520</i>.
The shared folder will be in the "File Explorers" workspace named "localhost". The shared folder will be in the "File Explorers" workspace named "localhost".
<br/>Read more about other <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#ports-usage">Remixd ports usage</a> <br/>Read more about other <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#ports-usage">Remixd ports usage</a>
</div> </div>
<div class=${css.dialogParagraph}> <div className='mb-2 text-break'>
This feature is still in Alpha. We recommend to keep a backup of the shared folder. This feature is still in Alpha. We recommend to keep a backup of the shared folder.
</div> </div>
<div class=${css.dialogParagraph}> <div className='mb-2 text-break'>
<h6 class="text-danger"> <h6 className="text-danger">
Before using, make sure remixd version is latest i.e. <b>${remixdVersion}</b> Before using, make sure remixd version is latest i.e. <b>${remixdVersion}</b>
<br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a> <br></br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a>
</h6> </h6>
</div> </div>
</div> </div>
` </>)
} }

@ -34,14 +34,14 @@ const profile = {
} }
export class RunTab extends ViewPlugin { export class RunTab extends ViewPlugin {
constructor (blockchain, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, mainView, fileProvider) { constructor (blockchain, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, fileProvider) {
super(profile) super(profile)
this.event = new EventManager() this.event = new EventManager()
this.config = config this.config = config
this.blockchain = blockchain this.blockchain = blockchain
this.fileManager = fileManager this.fileManager = fileManager
this.editor = editor this.editor = editor
this.logCallback = (msg) => { mainView.getTerminal().logHtml(yo`<pre>${msg}</pre>`) } this.logCallback = (msg) => { this.call('terminal', 'logHtml', yo`<pre>${msg}</pre>`) }
this.filePanel = filePanel this.filePanel = filePanel
this.compilersArtefacts = compilersArtefacts this.compilersArtefacts = compilersArtefacts
this.networkModule = networkModule this.networkModule = networkModule

@ -5,8 +5,6 @@ import * as packageJson from '../../../../../../package.json'
import { ViewPlugin } from '@remixproject/engine-web' import { ViewPlugin } from '@remixproject/engine-web'
import { RemixUiHomeTab } from '@remix-ui/home-tab' // eslint-disable-line import { RemixUiHomeTab } from '@remix-ui/home-tab' // eslint-disable-line
const GistHandler = require('../../../lib/gist-handler')
const profile = { const profile = {
name: 'home', name: 'home',
displayName: 'Home', displayName: 'Home',
@ -26,7 +24,6 @@ export class LandingPage extends ViewPlugin {
this.contentImport = contentImport this.contentImport = contentImport
this.appManager = appManager this.appManager = appManager
this.verticalIcons = verticalIcons this.verticalIcons = verticalIcons
this.gistHandler = new GistHandler()
this.el = document.createElement('div') this.el = document.createElement('div')
this.el.setAttribute('id', 'landingPageHomeContainer') this.el.setAttribute('id', 'landingPageHomeContainer')
this.el.setAttribute('class', 'remixui_homeContainer justify-content-between bg-light d-flex') this.el.setAttribute('class', 'remixui_homeContainer justify-content-between bg-light d-flex')

@ -1,34 +0,0 @@
export class FramingService {
constructor (sidePanel, verticalIcons, mainView, resizeFeature) {
this.sidePanel = sidePanel
this.verticalIcons = verticalIcons
this.mainPanel = mainView.getAppPanel()
this.mainView = mainView
this.resizeFeature = resizeFeature
}
start (params) {
this.verticalIcons.select('filePanel')
document.addEventListener('keypress', (e) => {
if (e.shiftKey && e.ctrlKey) {
if (e.code === 'KeyF') { // Ctrl+Shift+F
this.verticalIcons.select('filePanel')
} else if (e.code === 'KeyA') { // Ctrl+Shift+A
this.verticalIcons.select('pluginManager')
} else if (e.code === 'KeyS') { // Ctrl+Shift+S
this.verticalIcons.select('settings')
}
e.preventDefault()
}
})
if (params.minimizeterminal) this.mainView.minimizeTerminal()
if (params.minimizesidepanel) this.resizeFeature.hidePanel()
}
embed () {
this.mainView.minimizeTerminal()
this.resizeFeature.hidePanel()
}
}

@ -6,7 +6,6 @@ var async = require('async')
var EventManager = require('../lib/events') var EventManager = require('../lib/events')
var toolTip = require('../app/ui/tooltip') var toolTip = require('../app/ui/tooltip')
var GistHandler = require('./gist-handler')
class CmdInterpreterAPI { class CmdInterpreterAPI {
constructor (terminal, blockchain) { constructor (terminal, blockchain) {
@ -17,7 +16,6 @@ class CmdInterpreterAPI {
self._components.registry = Registry.getInstance() self._components.registry = Registry.getInstance()
self._components.terminal = terminal self._components.terminal = terminal
self._components.fileImport = new CompilerImports() self._components.fileImport = new CompilerImports()
self._components.gistHandler = new GistHandler()
self._deps = { self._deps = {
fileManager: self._components.registry.get('filemanager').api, fileManager: self._components.registry.get('filemanager').api,
editor: self._components.registry.get('editor').api, editor: self._components.registry.get('editor').api,
@ -35,8 +33,7 @@ class CmdInterpreterAPI {
log () { arguments[0] != null ? this._components.terminal.commands.html(arguments[0]) : this._components.terminal.commands.html(arguments[1]) } log () { arguments[0] != null ? this._components.terminal.commands.html(arguments[0]) : this._components.terminal.commands.html(arguments[1]) }
loadgist (id, cb) { loadgist (id, cb) {
const self = this this._components.terminal.call('gistHandler', 'load', id)
self._components.gistHandler.loadFromGist({ gist: id }, this._deps.fileManager)
if (cb) cb() if (cb) cb()
} }

@ -1,74 +0,0 @@
'use strict'
var modalDialogCustom = require('../app/ui/modal-dialog-custom')
var request = require('request')
// Allowing window to be overriden for testing
function GistHandler (_window) {
if (_window !== undefined) {
modalDialogCustom = _window
}
this.handleLoad = function (params, cb) {
if (!cb) cb = () => {}
var loadingFromGist = false
var gistId
if (params.gist === '') {
loadingFromGist = true
modalDialogCustom.prompt('Load a Gist', 'Enter the ID of the Gist or URL you would like to load.', null, (target) => {
if (target !== '') {
gistId = getGistId(target)
if (gistId) {
cb(gistId)
} else {
modalDialogCustom.alert('Gist load error', 'Error while loading gist. Please provide a valid Gist ID or URL.')
}
}
})
return loadingFromGist
} else {
gistId = params.gist
loadingFromGist = !!gistId
}
if (loadingFromGist) {
cb(gistId)
}
return loadingFromGist
}
function getGistId (str) {
var idr = /[0-9A-Fa-f]{8,}/
var match = idr.exec(str)
return match ? match[0] : null
}
this.loadFromGist = (params, fileManager) => {
const self = this
return self.handleLoad(params, function (gistId) {
request.get({
url: `https://api.github.com/gists/${gistId}`,
json: true
}, async (error, response, data = {}) => {
if (error || !data.files) {
modalDialogCustom.alert('Gist load error', error || data.message)
return
}
const obj = {}
Object.keys(data.files).forEach((element) => {
const path = element.replace(/\.\.\./g, '/')
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element]
})
fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => {
if (!errorLoadingFile) {
const provider = fileManager.getProvider('workspace')
provider.lastLoadedGistId = gistId
} else {
modalDialogCustom.alert('Gist load error', errorLoadingFile.message || errorLoadingFile)
}
})
})
})
}
}
module.exports = GistHandler

@ -1,88 +0,0 @@
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const css = csjs`
.dragbar {
width : 2px;
height : 100%;
cursor : col-resize;
z-index : 999;
}
.ghostbar {
width : 3px;
background-color : var(--primary);
opacity : 0.5;
position : absolute;
cursor : col-resize;
z-index : 9999;
top : 0;
bottom : 0;
}
`
export default class PanelsResize {
constructor (panel) {
this.panel = panel
const string = panel.style.minWidth
this.minWidth = string.length > 2 ? parseInt(string.substring(0, string.length - 2)) : 0
}
render () {
this.ghostbar = yo`<div class=${css.ghostbar}></div>`
const mousedown = (event) => {
event.preventDefault()
if (event.which === 1) {
moveGhostbar(event)
document.body.appendChild(this.ghostbar)
document.addEventListener('mousemove', moveGhostbar)
document.addEventListener('mouseup', removeGhostbar)
document.addEventListener('keydown', cancelGhostbar)
}
}
const cancelGhostbar = (event) => {
if (event.keyCode === 27) {
document.body.removeChild(this.ghostbar)
document.removeEventListener('mousemove', moveGhostbar)
document.removeEventListener('mouseup', removeGhostbar)
document.removeEventListener('keydown', cancelGhostbar)
}
}
const moveGhostbar = (event) => {
this.ghostbar.style.left = event.x + 'px'
}
const removeGhostbar = (event) => {
document.body.removeChild(this.ghostbar)
document.removeEventListener('mousemove', moveGhostbar)
document.removeEventListener('mouseup', removeGhostbar)
document.removeEventListener('keydown', cancelGhostbar)
this.setPosition(event)
}
return yo`<div onmousedown=${mousedown} class=${css.dragbar}></div>`
}
calculatePanelWidth (event) {
return event.x - this.panel.offsetLeft
}
setPosition (event) {
const panelWidth = this.calculatePanelWidth(event)
// close the panel if the width is less than a minWidth
if (panelWidth > this.minWidth - 10 || this.panel.style.display === 'none') {
this.panel.style.width = panelWidth + 'px'
this.showPanel()
} else this.hidePanel()
}
hidePanel () {
this.panel.style.display = 'none'
}
showPanel () {
this.panel.style.display = 'flex'
}
}

@ -1,20 +1,20 @@
/* global localStorage, fetch */ /* global localStorage, fetch */
import { PluginManager } from '@remixproject/engine' import { PluginManager } from '@remixproject/engine'
import { IframePlugin } from '@remixproject/engine-web'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import QueryParams from './lib/query-params' import QueryParams from './lib/query-params'
import { PermissionHandler } from './app/ui/persmission-handler' import { PermissionHandler } from './app/ui/persmission-handler'
import { IframePlugin } from '@remixproject/engine-web'
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const requiredModules = [ // services + layout views + system views const requiredModules = [ // services + layout views + system views
'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme',
'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', 'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons',
'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic'] 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic', 'gistHandler', 'layout', 'modal']
const dependentModules = ['git', 'hardhat', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd) const dependentModules = ['git', 'hardhat', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd)
export function isNative (name) { export function isNative (name) {
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'hardhat-provider', 'solidityStaticAnalysis', 'solidityUnitTesting'] const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'hardhat-provider', 'solidityStaticAnalysis', 'solidityUnitTesting', 'layout', 'modal']
return nativePlugins.includes(name) || requiredModules.includes(name) return nativePlugins.includes(name) || requiredModules.includes(name)
} }
@ -78,6 +78,7 @@ export class RemixAppManager extends PluginManager {
onPluginActivated (plugin) { onPluginActivated (plugin) {
this.pluginLoader.set(plugin, this.actives) this.pluginLoader.set(plugin, this.actives)
this.event.emit('activate', plugin) this.event.emit('activate', plugin)
this.emit('activate', plugin)
if (!requiredModules.includes(plugin.name)) _paq.push(['trackEvent', 'pluginManager', 'activate', plugin.name]) if (!requiredModules.includes(plugin.name)) _paq.push(['trackEvent', 'pluginManager', 'activate', plugin.name])
} }
@ -131,6 +132,7 @@ export class RemixAppManager extends PluginManager {
} }
return plugins.map(plugin => { return plugins.map(plugin => {
return new IframePlugin(plugin) return new IframePlugin(plugin)
// return new IframeReactPlugin(plugin)
}) })
} }

@ -1,16 +0,0 @@
'use strict'
var test = require('tape')
var Compiler = require('@remix-project/remix-solidity').Compiler
test('compiler.compile smoke', function (t) {
t.plan(1)
var noop = function () {}
var fakeImport = function (url, cb) { cb('Not implemented') }
var compiler = new Compiler(fakeImport)
compiler.compileJSON = noop
compiler.compile({ 'test': '' }, 'test')
t.ok(compiler)
})

@ -1,52 +0,0 @@
'use strict'
var modalDialogCustom
if (typeof window !== 'undefined') {
modalDialogCustom = require('../app/ui/modal-dialog-custom')
}
// ^ this class can be load in a non browser context when running node unit testing.
// should not load UI in that case
// Allowing window to be overriden for testing
function GistHandler (_window) {
if (_window !== undefined) {
modalDialogCustom = _window
}
this.handleLoad = function (params, cb) {
if (!cb) cb = () => {}
var loadingFromGist = false
var gistId
if (params['gist'] === '') {
loadingFromGist = true
modalDialogCustom.prompt(
'Load a Gist',
'Enter the URL or ID of the Gist you would like to load.',
null,
target => {
if (target !== '') {
gistId = getGistId(target)
if (gistId) {
cb(gistId)
}
}
}
)
return loadingFromGist
} else {
gistId = params['gist']
loadingFromGist = !!gistId
}
if (loadingFromGist) {
cb(gistId)
}
return loadingFromGist
}
function getGistId (str) {
var idr = /[0-9A-Fa-f]{8,}/
var match = idr.exec(str)
return match ? match[0] : null
}
}
module.exports = GistHandler

@ -1,5 +0,0 @@
'use strict'
require('./compiler-test')
require('./gist-handler-test')
require('./query-params-test')

@ -1,23 +0,0 @@
'use strict'
var test = require('tape')
var QueryParams = require('../src/lib/query-params')
test('queryParams.get', function (t) {
t.plan(2)
var fakeWindow = {location: {hash: '#wat=sup&foo=bar', search: ''}}
var params = new QueryParams(fakeWindow).get()
t.equal(params.wat, 'sup')
t.equal(params.foo, 'bar')
})
test('queryParams.update', function (t) {
t.plan(1)
var fakeWindow = {location: {hash: '#wat=sup', search: ''}}
var qp = new QueryParams(fakeWindow)
qp.update({foo: 'bar'})
t.equal(fakeWindow.location.hash, '#wat=sup&foo=bar')
})

@ -4,9 +4,11 @@
"jsx": "react", "jsx": "react",
"allowJs": true, "allowJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"types": ["node", "jest"], "types": ["node", "jest"],
"module": "es6", "module": "es6",
"resolveJsonModule": true
}, },
"files": [ "files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts", "../../node_modules/@nrwl/react/typings/cssmodule.d.ts",

@ -4,3 +4,4 @@ export { FetchAndCompile } from './lib/compiler-fetch-and-compile'
export { CompilerImports } from './lib/compiler-content-imports' export { CompilerImports } from './lib/compiler-content-imports'
export { CompilerArtefacts } from './lib/compiler-artefacts' export { CompilerArtefacts } from './lib/compiler-artefacts'
export { EditorContextListener } from './lib/editor-context-listener' export { EditorContextListener } from './lib/editor-context-listener'
export { GistHandler } from './lib/gist-handler'

@ -146,6 +146,8 @@ export class CompilerImports extends Plugin {
const splitted = /([^/]+)\/(.*)$/g.exec(url) const splitted = /([^/]+)\/(.*)$/g.exec(url)
const possiblePaths = ['localhost/installed_contracts/' + url] const possiblePaths = ['localhost/installed_contracts/' + url]
// pick remix-tests library contracts from '.deps'
if (url.startsWith('remix_')) possiblePaths.push('localhost/.deps/remix-tests/' + url)
if (splitted) possiblePaths.push('localhost/installed_contracts/' + splitted[1] + '/contracts/' + splitted[2]) if (splitted) possiblePaths.push('localhost/installed_contracts/' + splitted[1] + '/contracts/' + splitted[2])
possiblePaths.push('localhost/node_modules/' + url) possiblePaths.push('localhost/node_modules/' + url)
if (splitted) possiblePaths.push('localhost/node_modules/' + splitted[1] + '/contracts/' + splitted[2]) if (splitted) possiblePaths.push('localhost/node_modules/' + splitted[1] + '/contracts/' + splitted[2])

@ -0,0 +1,138 @@
/* global fetch */
'use strict'
import { Plugin } from '@remixproject/engine'
interface StringByString {
[key: string]: string;
}
const profile = {
name: 'gistHandler',
methods: ['load'],
events: [],
version: '0.0.1'
}
export class GistHandler extends Plugin {
constructor () {
super(profile)
}
async handleLoad (gistId: String | null, cb: Function) {
if (!cb) cb = () => {}
var loadingFromGist = false
if (!gistId) {
loadingFromGist = true
let value
try {
value = await (() => {
return new Promise((resolve, reject) => {
const modalContent = {
id: 'gisthandler',
title: 'Load a Gist',
message: 'Enter the ID of the Gist or URL you would like to load.',
modalType: 'prompt',
okLabel: 'OK',
cancelLabel: 'Cancel',
okFn: (value) => {
setTimeout(() => resolve(value), 0)
},
cancelFn: () => {
setTimeout(() => reject(new Error('Canceled')), 0)
},
hideFn: () => {
setTimeout(() => reject(new Error('Hide')), 0)
}
}
this.call('modal', 'modal', modalContent)
})
})()
} catch (e) {
// the modal has been canceled
return
}
if (value !== '') {
gistId = getGistId(value)
if (gistId) {
cb(gistId)
} else {
const modalContent = {
id: 'gisthandler',
title: 'Gist load error',
message: 'Error while loading gist. Please provide a valid Gist ID or URL.'
}
this.call('modal', 'alert', modalContent)
}
} else {
const modalContent = {
id: 'gisthandlerEmpty',
title: 'Gist load error',
message: 'Error while loading gist. Id cannot be empty.'
}
this.call('modal', 'alert', modalContent)
}
return loadingFromGist
} else {
loadingFromGist = !!gistId
}
if (loadingFromGist) {
cb(gistId)
}
return loadingFromGist
}
load (gistId: String | null) {
const self = this
return self.handleLoad(gistId, async (gistId: String | null) => {
let data: any
try {
data = await (await fetch(`https://api.github.com/gists/${gistId}`)).json() as any
if (!data.files) {
const modalContent = {
id: 'gisthandler',
title: 'Gist load error',
message: data.message,
modalType: 'alert',
okLabel: 'OK'
}
await this.call('modal', 'modal', modalContent)
return
}
} catch (e: any) {
const modalContent = {
id: 'gisthandler',
title: 'Gist load error',
message: e.message
}
await this.call('modal', 'alert', modalContent)
return
}
const obj: StringByString = {}
Object.keys(data.files).forEach((element) => {
const path = element.replace(/\.\.\./g, '/')
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element]
})
this.call('fileManager', 'setBatchFiles', obj, 'workspace', true, async (errorSavingFiles: any) => {
if (errorSavingFiles) {
const modalContent = {
id: 'gisthandler',
title: 'Gist load error',
message: errorSavingFiles.message || errorSavingFiles
}
this.call('modal', 'alert', modalContent)
}
})
})
}
}
const getGistId = (str) => {
var idr = /[0-9A-Fa-f]{8,}/
var match = idr.exec(str)
return match ? match[0] : null
}

@ -1 +1,6 @@
export { default as RemixApp } from './lib/remix-app/remix-app' export { default as RemixApp } from './lib/remix-app/remix-app'
export { dispatchModalContext } from './lib/remix-app/context/context'
export { ModalProvider } from './lib/remix-app/context/provider'
export { AppModal } from './lib/remix-app/interface/index'
export { AlertModal } from './lib/remix-app/interface/index'
export * from './lib/remix-app/types/index'

@ -0,0 +1,30 @@
import { AppModal } from '../interface'
type ActionMap<M extends { [index: string]: any }> = {
[Key in keyof M]: M[Key] extends undefined
? {
type: Key;
}
: {
type: Key;
payload: M[Key];
}
}
export const enum modalActionTypes {
setModal = 'SET_MODAL',
setToast = 'SET_TOAST',
handleHideModal = 'HANDLE_HIDE_MODAL',
handleToaster = 'HANDLE_HIDE_TOAST'
}
type ModalPayload = {
[modalActionTypes.setModal]: AppModal
[modalActionTypes.handleHideModal]: any
[modalActionTypes.setToast]: string
[modalActionTypes.handleToaster]: any
}
export type ModalAction = ActionMap<ModalPayload>[keyof ActionMap<
ModalPayload
>]

@ -0,0 +1,27 @@
/* dragbar UI */
.dragbar {
display: block;
height: 100%;
position: absolute;
left: 0px;
top: 0px;
width: 0.3em;
z-index: 9999;
}
.overlay {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
display: block;
z-index: 9998;
}
.dragbar:hover,
.dragbar.ondrag {
background-color: var(--secondary);
cursor: col-resize;
}

@ -15,18 +15,23 @@ const DragBar = (props: IRemixDragBarUi) => {
const [offset, setOffSet] = useState<number>(0) const [offset, setOffSet] = useState<number>(0)
const nodeRef = React.useRef(null) // fix for strictmode const nodeRef = React.useRef(null) // fix for strictmode
useEffect(() => {
// arbitrary time out to wait the the UI to be completely done
setTimeout(() => {
setOffSet(props.refObject.current.offsetLeft)
setDragBarPosX(offset + props.refObject.current.offsetWidth)
}, 1000)
}, [])
useEffect(() => { useEffect(() => {
setDragBarPosX(offset + (props.hidden ? 0 : props.refObject.current.offsetWidth)) setDragBarPosX(offset + (props.hidden ? 0 : props.refObject.current.offsetWidth))
}, [props.hidden, offset]) }, [props.hidden, offset])
const handleResize = () => {
setOffSet(props.refObject.current.offsetLeft)
setDragBarPosX(props.refObject.current.offsetLeft + props.refObject.current.offsetWidth)
}
useEffect(() => {
window.addEventListener('resize', handleResize)
// TODO: not a good way to wait on the ref doms element to be rendered of course
setTimeout(() =>
handleResize(), 2000)
return () => window.removeEventListener('resize', handleResize)
}, [])
function stopDrag (e: MouseEvent, data: any) { function stopDrag (e: MouseEvent, data: any) {
setDragState(false) setDragState(false)
if (data.x < props.minWidth) { if (data.x < props.minWidth) {

@ -0,0 +1,15 @@
import React, { useContext, useEffect } from 'react'
import { AppContext } from '../../context/context'
import { useDialogDispatchers } from '../../context/provider'
const DialogViewPlugin = () => {
const { modal, alert, toast } = useDialogDispatchers()
const app = useContext(AppContext)
useEffect(() => {
app.modal.setDispatcher({ modal, alert, toast })
}, [])
return <></>
}
export default DialogViewPlugin

@ -0,0 +1,16 @@
import React from 'react'
import { useDialogDispatchers, useDialogs } from '../../context/provider'
import { Toaster } from '@remix-ui/toaster'
import ModalWrapper from './modal-wrapper'
const AppDialogs = () => {
const { handleHideModal, handleToaster } = useDialogDispatchers()
const { focusModal, focusToaster } = useDialogs()
return (
<>
<ModalWrapper {...focusModal} handleHide={handleHideModal}></ModalWrapper>
<Toaster message={focusToaster} handleHide={handleToaster} />
</>)
}
export default AppDialogs

@ -0,0 +1,45 @@
import React, { useContext, useEffect, useState } from 'react'
import { AppContext } from '../../context/context'
import { useDialogDispatchers } from '../../context/provider'
const _paq = window._paq = window._paq || []
const MatomoDialog = (props) => {
const { settings, showMatamo, appManager } = useContext(AppContext)
const { modal } = useDialogDispatchers()
const [visible, setVisible] = useState<boolean>(props.hide)
const message = () => {
return (<><p>An Opt-in version of <a href="https://matomo.org" target="_blank" rel="noreferrer">Matomo</a>, an open source data analytics platform is being used to improve Remix IDE.</p>
<p>We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.</p>
<p>All data collected through Matomo is stored on our own server - no data is ever given to third parties. Our analytics reports are public: <a href="https://matomo.ethereum.org/index.php?module=MultiSites&action=index&idSite=23&period=day&date=yesterday" target="_blank" rel="noreferrer">take a look</a>.</p>
<p>We do not collect nor store any personally identifiable information (PII).</p>
<p>For more info, see: <a href="https://medium.com/p/66ef69e14931/" target="_blank" rel="noreferrer">Matomo Analyitcs on Remix iDE</a>.</p>
<p>You can change your choice in the Settings panel anytime.</p></>)
}
useEffect(() => {
if (visible && showMatamo) {
modal({ id: 'matomoModal', title: 'Help us to improve Remix IDE', message: message(), okLabel: 'Accept', okFn: handleModalOkClick, cancelLabel: 'Decline', cancelFn: declineModal })
}
}, [visible])
const declineModal = async () => {
settings.updateMatomoAnalyticsChoice(false)
_paq.push(['optUserOut'])
appManager.call('walkthrough', 'start')
setVisible(false)
}
const handleModalOkClick = async () => {
_paq.push(['forgetUserOptOut'])
// @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
settings.updateMatomoAnalyticsChoice(true)
appManager.call('walkthrough', 'start')
setVisible(false)
}
return (<></>)
}
export default MatomoDialog

@ -0,0 +1,56 @@
import React, { useEffect, useRef, useState } from 'react'
import { ModalDialog } from '@remix-ui/modal-dialog'
import { ModalDialogProps } from 'libs/remix-ui/modal-dialog/src/lib/types'
import { ModalTypes } from '../../types'
interface ModalWrapperProps extends ModalDialogProps {
modalType?: ModalTypes
defaultValue?: string
}
const ModalWrapper = (props: ModalWrapperProps) => {
const [state, setState] = useState<ModalDialogProps>()
const ref = useRef()
const onFinishPrompt = async () => {
if (ref.current === undefined) {
props.okFn()
} else {
// @ts-ignore: Object is possibly 'null'.
props.okFn(ref.current.value)
}
}
const createModalMessage = (defaultValue: string) => {
return (
<>
{props.message}
<input type={props.modalType === ModalTypes.password ? 'password' : 'text'} defaultValue={defaultValue} data-id="modalDialogCustomPromp" ref={ref} className="form-control" /></>
)
}
useEffect(() => {
if (props.modalType) {
switch (props.modalType) {
case ModalTypes.prompt:
case ModalTypes.password:
setState({
...props,
okFn: onFinishPrompt,
message: createModalMessage(props.defaultValue)
})
break
default:
setState({ ...props })
break
}
} else {
setState({ ...props })
}
}, [props])
return (
<ModalDialog id={props.id} {...state} handleHide={props.handleHide} />
)
}
export default ModalWrapper

@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { ModalDialog } from '@remix-ui/modal-dialog' import { ModalDialog } from '@remix-ui/modal-dialog'
import { useDialogDispatchers } from '../../context/provider'
const AlertModal = () => { const OriginWarning = () => {
const [visible, setVisible] = useState<boolean>(true) const { alert } = useDialogDispatchers()
const [content, setContent] = useState<string>('') const [content, setContent] = useState<string>(null)
useEffect(() => { useEffect(() => {
// check the origin and warn message // check the origin and warn message
@ -20,24 +21,15 @@ const AlertModal = () => {
This instance of Remix you are visiting WILL NOT BE UPDATED.\n This instance of Remix you are visiting WILL NOT BE UPDATED.\n
Please make a backup of your contracts and start using http://remix.ethereum.org`) Please make a backup of your contracts and start using http://remix.ethereum.org`)
} }
setVisible(content !== '')
}, []) }, [])
const closeModal = async () => { useEffect(() => {
setVisible(false) if (content) {
} alert({ id: 'warningOriging', title: null, message: content })
const handleModalOkClick = async () => { }
setVisible(false) }, [content])
}
return (<ModalDialog return (<></>)
handleHide={closeModal}
id="appAlert"
hide={!visible}
title="Alert"
okLabel="Ok"
okFn={ handleModalOkClick }
cancelLabel=""
cancelFn={closeModal}>{content}</ModalDialog>)
} }
export default AlertModal export default OriginWarning

@ -1,5 +1,24 @@
import React from 'react' import React from 'react'
import { AlertModal, AppModal } from '../interface'
import { ModalInitialState } from '../state/modals'
import { ModalTypes } from '../types'
const AppContext = React.createContext(null) export const AppContext = React.createContext<any>(null)
export default AppContext export interface dispatchModalInterface {
modal: (data: AppModal) => void
toast: (message: string) => void
alert: (data: AlertModal) => void
handleHideModal: () => void,
handleToaster: () => void
}
export const dispatchModalContext = React.createContext<dispatchModalInterface>({
modal: (data: AppModal) => { },
toast: (message: string) => {},
alert: (data: AlertModal) => {},
handleHideModal: () => {},
handleToaster: () => {}
})
export const modalContext = React.createContext(ModalInitialState)

@ -0,0 +1,64 @@
import React, { useReducer } from 'react'
import { modalActionTypes } from '../actions/modals'
import { AlertModal, AppModal } from '../interface'
import { modalReducer } from '../reducer/modals'
import { ModalInitialState } from '../state/modals'
import { ModalTypes } from '../types'
import { AppContext, dispatchModalContext, modalContext } from './context'
export const ModalProvider = ({ children = [], reducer = modalReducer, initialState = ModalInitialState } = {}) => {
const [{ modals, toasters, focusModal, focusToaster }, dispatch] = useReducer(reducer, initialState)
const modal = (data: AppModal) => {
const { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType, defaultValue, hideFn } = data
dispatch({
type: modalActionTypes.setModal,
payload: { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType: modalType || ModalTypes.default, defaultValue: defaultValue, hideFn }
})
}
const alert = (data: AlertModal) => {
modal({ id: data.id, title: data.title || 'Alert', message: data.message || data.title, okLabel: 'OK', okFn: (value?:any) => {}, cancelLabel: '', cancelFn: () => {} })
}
const handleHideModal = () => {
dispatch({
type: modalActionTypes.handleHideModal,
payload: null
})
}
const toast = (message: string) => {
dispatch({
type: modalActionTypes.setToast,
payload: message
})
}
const handleToaster = () => {
dispatch({
type: modalActionTypes.handleToaster,
payload: null
})
}
return (<dispatchModalContext.Provider value={{ modal, toast, alert, handleHideModal, handleToaster }}>
<modalContext.Provider value={{ modals, toasters, focusModal, focusToaster }}>
{children}
</modalContext.Provider>
</dispatchModalContext.Provider>)
}
export const AppProvider = ({ children = [], value = {} } = {}) => {
return <AppContext.Provider value={value}>
<ModalProvider>{children}</ModalProvider>
</AppContext.Provider>
}
export const useDialogs = () => {
return React.useContext(modalContext)
}
export const useDialogDispatchers = () => {
return React.useContext(dispatchModalContext)
}

@ -1,26 +0,0 @@
/* dragbar UI */
.dragbar {
display : block;
height : 100%;
position : absolute;
left: 0px;
top: 0px;
width: 0.3em;
z-index: 9999;
}
.overlay {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
display: block;
z-index: 9998;
}
.dragbar:hover, .dragbar.ondrag{
background-color: var(--secondary);
cursor:col-resize;
}

@ -0,0 +1,29 @@
import { ModalTypes } from '../types'
export interface AppModal {
id: string
hide?: boolean
title: string
// eslint-disable-next-line no-undef
message: string | JSX.Element
okLabel: string
okFn: (value?:any) => void
cancelLabel: string
cancelFn: () => void,
modalType?: ModalTypes,
defaultValue?: string
hideFn?: () => void
}
export interface AlertModal {
id: string
title?: string,
message: string | JSX.Element,
}
export interface ModalState {
modals: AppModal[],
toasters: string[],
focusModal: AppModal,
focusToaster: string
}

@ -1,51 +0,0 @@
import React, { useContext, useEffect, useState } from 'react'
import { ModalDialog } from '@remix-ui/modal-dialog'
import AppContext from '../context/context'
const _paq = window._paq = window._paq || []
const MatomoDialog = (props) => {
const { settings, showMatamo, appManager } = useContext(AppContext)
const [visible, setVisible] = useState<boolean>(props.hide)
useEffect(() => {
if (showMatamo) {
setVisible(true)
} else {
setVisible(false)
}
}, [])
const declineModal = async () => {
settings.updateMatomoAnalyticsChoice(false)
_paq.push(['optUserOut'])
appManager.call('walkthrough', 'start')
setVisible(false)
}
const hideModal = async () => {
setVisible(false)
}
const handleModalOkClick = async () => {
_paq.push(['forgetUserOptOut'])
// @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
settings.updateMatomoAnalyticsChoice(true)
appManager.call('walkthrough', 'start')
setVisible(false)
}
return (<ModalDialog
handleHide={hideModal}
id="matomoDialog"
hide={!visible}
title="Help us to improve Remix IDE"
okLabel="Accept"
okFn={ handleModalOkClick }
cancelLabel="Decline"
cancelFn={declineModal}>
<p>An Opt-in version of <a href="https://matomo.org" target="_blank" rel="noreferrer">Matomo</a>, an open source data analytics platform is being used to improve Remix IDE.</p>
<p>We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.</p>
<p>All data collected through Matomo is stored on our own server - no data is ever given to third parties. Our analytics reports are public: <a href="https://matomo.ethereum.org/index.php?module=MultiSites&action=index&idSite=23&period=day&date=yesterday" target="_blank" rel="noreferrer">take a look</a>.</p>
<p>We do not collect nor store any personally identifiable information (PII).</p>
<p>For more info, see: <a href="https://medium.com/p/66ef69e14931/" target="_blank" rel="noreferrer">Matomo Analyitcs on Remix iDE</a>.</p>
<p>You can change your choice in the Settings panel anytime.</p>
</ModalDialog>)
}
export default MatomoDialog

@ -0,0 +1,50 @@
import { modalActionTypes, ModalAction } from '../actions/modals'
import { ModalInitialState } from '../state/modals'
import { AppModal, ModalState } from '../interface'
export const modalReducer = (state: ModalState = ModalInitialState, action: ModalAction) => {
switch (action.type) {
case modalActionTypes.setModal: {
let modalList:AppModal[] = state.modals
modalList.push(action.payload)
if (state.modals.length === 1 && state.focusModal.hide === true) { // if it's the first one show it
const focusModal: AppModal = {
id: modalList[0].id,
hide: false,
title: modalList[0].title,
message: modalList[0].message,
okLabel: modalList[0].okLabel,
okFn: modalList[0].okFn,
cancelLabel: modalList[0].cancelLabel,
cancelFn: modalList[0].cancelFn,
modalType: modalList[0].modalType,
defaultValue: modalList[0].defaultValue,
hideFn: modalList[0].hideFn
}
modalList = modalList.slice()
modalList.shift()
return { ...state, modals: modalList, focusModal: focusModal }
}
return { ...state, modals: modalList }
}
case modalActionTypes.handleHideModal:
if (state.focusModal.hideFn) {
state.focusModal.hideFn()
}
state.focusModal = { ...state.focusModal, hide: true, message: null }
return { ...state }
case modalActionTypes.setToast:
state.toasters.push(action.payload)
if (state.toasters.length > 0) {
const focus = state.toasters[0]
state.toasters.shift()
return { ...state, focusToaster: focus }
}
return { ...state }
case modalActionTypes.handleToaster:
return { ...state, focusToaster: '' }
}
}

@ -1,10 +1,14 @@
import React, { useContext, useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import './style/remix-app.css' import './style/remix-app.css'
import RemixSplashScreen from './modals/splashscreen' import { RemixUIMainPanel } from '@remix-ui/panel'
import MatomoDialog from './modals/matomo' import RemixSplashScreen from './components/splashscreen'
import AlertModal from './modals/alert' import MatomoDialog from './components/modals/matomo'
import AppContext from './context/context' import OriginWarning from './components/modals/origin-warning'
import DragBar from './dragbar/dragbar' import DragBar from './components/dragbar/dragbar'
import { AppProvider } from './context/provider'
import AppDialogs from './components/modals/dialogs'
import DialogViewPlugin from './components/modals/dialogViewPlugin'
interface IRemixAppUi { interface IRemixAppUi {
app: any app: any
} }
@ -59,6 +63,13 @@ const RemixApp = (props: IRemixAppUi) => {
props.app.sidePanel.events.on('showing', () => { props.app.sidePanel.events.on('showing', () => {
setHideSidePanel(false) setHideSidePanel(false)
}) })
props.app.layout.event.on('minimizesidepanel', () => {
// the 'showing' event always fires from sidepanel, so delay this a bit
setTimeout(() => {
setHideSidePanel(true)
}, 1000)
})
} }
const components = { const components = {
@ -68,22 +79,32 @@ const RemixApp = (props: IRemixAppUi) => {
hiddenPanel: <div ref={hiddenPanelRef}></div> hiddenPanel: <div ref={hiddenPanelRef}></div>
} }
const value = {
settings: props.app.settings,
showMatamo: props.app.showMatamo,
appManager: props.app.appManager,
modal: props.app.modal,
layout: props.app.layout
}
return ( return (
<AppContext.Provider value={{ settings: props.app.settings, showMatamo: props.app.showMatamo, appManager: props.app.appManager }}> <AppProvider value={value}>
<RemixSplashScreen hide={appReady}></RemixSplashScreen> <RemixSplashScreen hide={appReady}></RemixSplashScreen>
<AlertModal></AlertModal> <OriginWarning></OriginWarning>
<MatomoDialog hide={!appReady}></MatomoDialog> <MatomoDialog hide={!appReady}></MatomoDialog>
<div className={`remixIDE ${appReady ? '' : 'd-none'}`} data-id="remixIDE"> <div className={`remixIDE ${appReady ? '' : 'd-none'}`} data-id="remixIDE">
{components.iconPanel} {components.iconPanel}
{components.sidePanel} {components.sidePanel}
<DragBar minWidth={250} refObject={sidePanelRef} hidden={hideSidePanel} setHideStatus={setHideSidePanel}></DragBar> <DragBar minWidth={250} refObject={sidePanelRef} hidden={hideSidePanel} setHideStatus={setHideSidePanel}></DragBar>
{components.mainPanel} <div id="main-panel" data-id="remixIdeMainPanel" className='mainpanel'>
<RemixUIMainPanel></RemixUIMainPanel>
</div>
</div> </div>
{components.hiddenPanel} {components.hiddenPanel}
</AppContext.Provider> <AppDialogs></AppDialogs>
<DialogViewPlugin></DialogViewPlugin>
</AppProvider>
) )
} }

@ -0,0 +1,17 @@
import { ModalState } from '../interface'
export const ModalInitialState: ModalState = {
modals: [],
toasters: [],
focusModal: {
id: '',
hide: true,
title: '',
message: '',
okLabel: '',
okFn: () => { },
cancelLabel: '',
cancelFn: () => { }
},
focusToaster: ''
}

@ -0,0 +1,7 @@
export const enum ModalTypes {
alert = 'alert',
confirm = 'confirm',
prompt = 'prompt',
password = 'password',
default = 'default',
}

@ -136,7 +136,7 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
plugin.appManager.activatePlugin('remixd') plugin.appManager.activatePlugin('remixd')
} }
const importFromGist = () => { const importFromGist = () => {
plugin.gistHandler.loadFromGist({ gist: '' }, fileManager) plugin.call('gistHandler', 'load', '')
plugin.verticalIcons.select('filePanel') plugin.verticalIcons.select('filePanel')
} }
const switchToPreviousVersion = () => { const switchToPreviousVersion = () => {

@ -12,12 +12,15 @@ export const ModalDialog = (props: ModalDialogProps) => {
const [state, setState] = useState({ const [state, setState] = useState({
toggleBtn: true toggleBtn: true
}) })
const calledHideFunctionOnce = useRef<boolean>()
const modal = useRef(null) const modal = useRef(null)
const handleHide = () => { const handleHide = () => {
props.handleHide() if (!calledHideFunctionOnce.current) { props.handleHide() }
calledHideFunctionOnce.current = true
} }
useEffect(() => { useEffect(() => {
calledHideFunctionOnce.current = props.hide
modal.current.focus() modal.current.focus()
}, [props.hide]) }, [props.hide])
@ -32,12 +35,9 @@ export const ModalDialog = (props: ModalDialogProps) => {
} }
if (modal.current) { if (modal.current) {
modal.current.addEventListener('blur', handleBlur) modal.current.addEventListener('blur', handleBlur)
}
return () => { return () => {
if (modal.current) { modal.current.removeEventListener('blur', handleBlur)
modal.current.removeEventListener('blur', handleBlur)
}
}
} }
}, [modal.current]) }, [modal.current])
@ -86,40 +86,38 @@ export const ModalDialog = (props: ModalDialogProps) => {
{props.title && props.title} {props.title && props.title}
</h6> </h6>
{!props.showCancelIcon && {!props.showCancelIcon &&
<span className="modal-close" onClick={() => handleHide()}> <span className="modal-close" onClick={() => handleHide()}>
<i title="Close" className="fas fa-times" aria-hidden="true"></i> <i title="Close" className="fas fa-times" aria-hidden="true"></i>
</span> </span>
} }
</div> </div>
<div className="modal-body text-break remixModalBody" data-id={`${props.id}ModalDialogModalBody-react`}> <div className="modal-body text-break remixModalBody" data-id={`${props.id}ModalDialogModalBody-react`}>
{ props.children ? props.children : props.message } {props.children ? props.children : props.message}
</div> </div>
<div className="modal-footer" data-id={`${props.id}ModalDialogModalFooter-react`}> <div className="modal-footer" data-id={`${props.id}ModalDialogModalFooter-react`}>
{/* todo add autofocus ^^ */} {/* todo add autofocus ^^ */}
{ props.okLabel && { props.okLabel && <span
<span data-id={`${props.id}-modal-footer-ok-react`}
data-id={`${props.id}-modal-footer-ok-react`} className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')}
className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')} onClick={() => {
onClick={() => { if (props.okFn) props.okFn()
if (props.okFn) props.okFn() handleHide()
handleHide() }}
}} >
> {props.okLabel ? props.okLabel : 'OK'}
{ props.okLabel ? props.okLabel : 'OK' } </span>
</span>
} }
{ props.cancelLabel && { props.cancelLabel && <span
<span data-id={`${props.id}-modal-footer-cancel-react`}
data-id={`${props.id}-modal-footer-cancel-react`} className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')}
className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')} data-dismiss="modal"
data-dismiss="modal" onClick={() => {
onClick={() => { if (props.cancelFn) props.cancelFn()
if (props.cancelFn) props.cancelFn() handleHide()
handleHide() }}
}} >
> {props.cancelLabel ? props.cancelLabel : 'Cancel'}
{ props.cancelLabel ? props.cancelLabel : 'Cancel' } </span>
</span>
} }
</div> </div>
</div> </div>

@ -4,7 +4,7 @@ export interface ModalDialogProps {
title?: string, title?: string,
message?: string | JSX.Element, message?: string | JSX.Element,
okLabel?: string, okLabel?: string,
okFn?: () => void, okFn?: (value?:any) => void,
cancelLabel?: string, cancelLabel?: string,
cancelFn?: () => void, cancelFn?: () => void,
modalClass?: string, modalClass?: string,

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

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

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

@ -0,0 +1,2 @@
export { default as RemixPluginPanel } from './lib/plugins/remix-ui-panel'
export { default as RemixUIMainPanel } from './lib/main/main-panel'

@ -0,0 +1,27 @@
/* dragbar UI */
.dragbar_terminal {
display: block;
width: 100%;
position: absolute;
left: 0px;
top: 0px;
height: 0.3em;
z-index: 9999;
}
.overlay {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
display: block;
z-index: 900;
}
.dragbar_terminal:hover,
.dragbar_terminal.ondrag {
background-color: var(--secondary);
cursor: row-resize;
}

@ -0,0 +1,51 @@
// eslint-disable-next-line no-use-before-define
import React, { useEffect, useState } from 'react'
import Draggable from 'react-draggable'
import './dragbar.css'
interface IRemixDragBarUi {
refObject: React.MutableRefObject<any>;
setHideStatus: (hide: boolean) => void;
hidden: boolean
minHeight?: number
}
const DragBar = (props: IRemixDragBarUi) => {
const [dragState, setDragState] = useState<boolean>(false)
const [dragBarPosY, setDragBarPosY] = useState<number>(0)
const nodeRef = React.useRef(null) // fix for strictmode
function stopDrag (e: MouseEvent, data: any) {
const h = window.innerHeight - data.y
props.refObject.current.setAttribute('style', `height: ${h}px;`)
setDragBarPosY(window.innerHeight - props.refObject.current.offsetHeight)
setDragState(false)
}
const handleResize = () => {
setDragBarPosY(window.innerHeight - props.refObject.current.offsetHeight)
}
useEffect(() => {
handleResize()
}, [props.hidden])
useEffect(() => {
window.addEventListener('resize', handleResize)
// TODO: not a good way to wait on the ref doms element to be rendered of course
setTimeout(() =>
handleResize(), 2000)
return () => window.removeEventListener('resize', handleResize)
}, [])
function startDrag () {
setDragState(true)
}
return <>
<div className={`overlay ${dragState ? '' : 'd-none'}`} ></div>
<Draggable nodeRef={nodeRef} position={{ x: 0, y: dragBarPosY }} onStart={startDrag} onStop={stopDrag} axis="y">
<div ref={nodeRef} className={`dragbar_terminal ${dragState ? 'ondrag' : ''}`}></div>
</Draggable>
</>
}
export default DragBar

@ -0,0 +1,8 @@
.mainview {
display : flex;
flex-direction : column;
height : 100%;
width : 100%;
position: relative;
}

@ -0,0 +1,60 @@
/* eslint-disable no-unused-expressions */
import { AppContext } from 'libs/remix-ui/app/src/lib/remix-app/context/context'
import React, { useContext, useEffect, useLayoutEffect, useRef, useState } from 'react' // eslint-disable-line
import DragBar from '../dragbar/dragbar'
import RemixUIPanelPlugin from '../plugins/panel-plugin'
import { PluginRecord } from '../types'
import './main-panel.css'
const RemixUIMainPanel = () => {
const appContext = useContext(AppContext)
const [plugins, setPlugins] = useState<PluginRecord[]>([])
const editorRef = useRef<HTMLDivElement>(null)
const mainPanelRef = useRef<HTMLDivElement>(null)
const tabsRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<HTMLDivElement>(null)
const refs = [tabsRef, editorRef, mainPanelRef, terminalRef]
const renderPanels = () => {
if (appContext) {
const pluginPanels: PluginRecord[] = []
Object.values(appContext.layout.panels).map((panel: any) => {
pluginPanels.push({
profile: panel.plugin.profile,
active: panel.active,
view: panel.plugin.profile.name === 'tabs' ? panel.plugin.renderTabsbar() : panel.plugin.render(),
class: panel.plugin.profile.name + '-wrap ' + (panel.minimized ? 'minimized' : ''),
minimized: panel.minimized
})
})
setPlugins(pluginPanels)
}
}
useEffect(() => {
renderPanels()
appContext.layout.event.on('change', () => {
renderPanels()
})
}, [])
return (
<div className="mainview">
{Object.values(plugins).map((pluginRecord, i) => {
return (
<React.Fragment key={`mainView${i}`}>
{(pluginRecord.profile.name === 'terminal') ? <DragBar key='dragbar-terminal' hidden={pluginRecord.minimized || false} setHideStatus={() => {}} refObject={terminalRef}></DragBar> : null}
<RemixUIPanelPlugin
ref={refs[i]}
key={pluginRecord.profile.name}
pluginRecord={pluginRecord}
/>
</React.Fragment>
)
})}
</div>
)
}
export default RemixUIMainPanel

@ -0,0 +1,27 @@
/* eslint-disable jsx-a11y/anchor-has-content */
import React, { useEffect, useRef, useState } from 'react' // eslint-disable-line
import { PluginRecord } from '../types'
import './panel.css'
export interface RemixPanelProps {
plugins: Record<string, PluginRecord>;
}
const RemixUIPanelHeader = (props: RemixPanelProps) => {
const [plugin, setPlugin] = useState<PluginRecord>()
useEffect(() => {
if (props.plugins) {
const p = Object.values(props.plugins).find((pluginRecord) => {
return pluginRecord.active === true
})
setPlugin(p)
}
}, [props])
return (
<header className='swapitHeader'><h6 data-id='sidePanelSwapitTitle'>{plugin?.profile.displayName || plugin?.profile.name}</h6>
{plugin?.profile.documentation ? (<a href={plugin.profile.documentation} className="titleInfo" title="link to documentation" target="_blank" rel="noreferrer"><i aria-hidden="true" className="fas fa-book"></i></a>) : ''}
</header>)
}
export default RemixUIPanelHeader

@ -0,0 +1,37 @@
/* eslint-disable no-undef */
import React, { forwardRef, useEffect, useRef, useState } from 'react' // eslint-disable-line
import { PluginRecord } from '../types'
import './panel.css'
interface panelPLuginProps {
pluginRecord: PluginRecord
}
const RemixUIPanelPlugin = (props: panelPLuginProps, panelRef: any) => {
const localRef = useRef<HTMLDivElement>(null)
const [view, setView] = useState<JSX.Element | HTMLDivElement>()
useEffect(() => {
const ref:any = panelRef || localRef
if (ref.current) {
if (props.pluginRecord.view) {
if (React.isValidElement(props.pluginRecord.view)) {
setView(props.pluginRecord.view)
} else {
ref.current.appendChild(props.pluginRecord.view)
}
}
}
}, [])
return (
<div
className={
props.pluginRecord.active ? `${props.pluginRecord.class}` : 'd-none'
}
ref={panelRef || localRef}
>
{view}
</div>
)
}
export default forwardRef(RemixUIPanelPlugin)

@ -0,0 +1,110 @@
.panel {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex: auto;
}
.swapitTitle {
margin: 0;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.swapitTitle i {
padding-left: 6px;
font-size: 14px;
}
.swapitHeader {
display: flex;
align-items: center;
padding: 16px 24px 15px;
justify-content: space-between;
text-transform: uppercase;
}
.icons i {
height: 80%;
cursor: pointer;
}
.pluginsContainer {
height: 100%;
overflow-y: auto;
}
.titleInfo {
padding-left: 10px;
}
.versionBadge {
background-color: var(--light);
padding: 0 7px;
font-weight: bolder;
margin-left: 5px;
text-transform: lowercase;
cursor: default;
}
iframe {
height: 100%;
width: 100%;
border: 0;
}
.plugins {
height: 100%;
}
.plugItIn {
display: none;
height: 100%;
}
.plugItIn>div {
overflow-y: auto;
overflow-x: hidden;
height: 100%;
width: 100%;
}
.plugItIn.active {
display: block;
}
.pluginsContainer {
height: 100%;
overflow-y: hidden;
}
#editorView {
height: 100%;
width: 100%;
border: 0;
display: block;
}
#mainPanel {
height: 100%;
width: 100%;
border: 0;
display: block;
}
.mainPanel-wrap, .editor-wrap {
flex: 1;
min-height: 100px;
}
.terminal-wrap {
min-height: 35px;
height: 20%;
}
.terminal-wrap.minimized {
height: 2rem !important;
}

@ -0,0 +1,29 @@
/* eslint-disable no-undef */
import React, { useEffect, useState } from 'react' // eslint-disable-line
import './panel.css'
import RemixUIPanelPlugin from './panel-plugin'
import { PluginRecord } from '../types'
/* eslint-disable-next-line */
export interface RemixPanelProps {
plugins: Record<string, PluginRecord>
header: JSX.Element
}
export function RemixPluginPanel (props: RemixPanelProps) {
return (
<>
{props.header}
<div className="pluginsContainer">
<div className='plugins' id='plugins'>
{Object.values(props.plugins).map((pluginRecord) => {
return <RemixUIPanelPlugin key={pluginRecord.profile.name} pluginRecord={pluginRecord} />
})}
</div>
</div>
</>
)
}
export default RemixPluginPanel

@ -0,0 +1,9 @@
import { Profile } from '@remixproject/plugin-utils'
export type PluginRecord = {
profile: Profile
view: any
active: boolean
class?: string
minimized?: boolean
}

@ -0,0 +1,20 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": 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"]
}

@ -4,6 +4,7 @@ import { EventEmitter } from 'events'
import { Engine } from '@remixproject/engine/lib/engine' import { Engine } from '@remixproject/engine/lib/engine'
import { PluginBase, Profile } from '@remixproject/plugin-utils' import { PluginBase, Profile } from '@remixproject/plugin-utils'
import { IframePlugin, ViewPlugin, WebsocketPlugin } from '@remixproject/engine-web' import { IframePlugin, ViewPlugin, WebsocketPlugin } from '@remixproject/engine-web'
import { IframeReactPlugin } from '@remix-ui/app'
/* eslint-disable camelcase */ /* eslint-disable camelcase */
interface SetPluginOptionType { interface SetPluginOptionType {
@ -88,7 +89,7 @@ export class PluginManagerComponent extends ViewPlugin extends Plugin implements
render(): HTMLDivElement render(): HTMLDivElement
getAndFilterPlugins: (filter?: string, profiles?: Profile[]) => void getAndFilterPlugins: (filter?: string, profiles?: Profile[]) => void
triggerEngineEventListener: () => void triggerEngineEventListener: () => void
activateAndRegisterLocalPlugin: (localPlugin: IframePlugin | WebsocketPlugin) => Promise<void> activateAndRegisterLocalPlugin: (localPlugin: IframePlugin | IframeReactPlugin | WebsocketPlugin) => Promise<void>
activeProfiles: string[] activeProfiles: string[]
_paq: any _paq: any
} }

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

@ -1,9 +1,6 @@
{ {
"extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"], "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"], "ignorePatterns": ["!**/*"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
},
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],

@ -5,6 +5,7 @@ export class TestTabLogic {
fileManager fileManager
currentPath currentPath
helper helper
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor (fileManager: any, helper: any) { constructor (fileManager: any, helper: any) {
this.fileManager = fileManager this.fileManager = fileManager
this.helper = helper this.helper = helper
@ -26,7 +27,7 @@ export class TestTabLogic {
if (!path || !(/\S/.test(path))) return if (!path || !(/\S/.test(path))) return
path = this.helper.removeMultipleSlashes(path) path = this.helper.removeMultipleSlashes(path)
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
fileProvider.exists(path).then((res: any) => { fileProvider.exists(path).then((res: boolean) => {
if (!res) fileProvider.createDir(path) if (!res) fileProvider.createDir(path)
}) })
} }
@ -35,11 +36,12 @@ export class TestTabLogic {
// Checking to ignore the value which contains only whitespaces // Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return if (!path || !(/\S/.test(path))) return
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
const res = await fileProvider.exists(path, (e: any, res: any) => { return res }) const res = await fileProvider.exists(path, (e: Error, res: boolean) => { return res })
return res return res
} }
generateTestFile (errorCb: any) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
generateTestFile (errorCb:any) {
let fileName = this.fileManager.currentFile() let fileName = this.fileManager.currentFile()
const hasCurrent = !!fileName && this.fileManager.currentFile().split('.').pop().toLowerCase() === 'sol' const hasCurrent = !!fileName && this.fileManager.currentFile().split('.').pop().toLowerCase() === 'sol'
if (!hasCurrent) fileName = this.currentPath + '/newFile.sol' if (!hasCurrent) fileName = this.currentPath + '/newFile.sol'
@ -47,7 +49,7 @@ export class TestTabLogic {
if (!fileProvider) return if (!fileProvider) return
const splittedFileName = fileName.split('/') const splittedFileName = fileName.split('/')
const fileNameToImport = (!hasCurrent) ? fileName : this.currentPath + '/' + splittedFileName[splittedFileName.length - 1] const fileNameToImport = (!hasCurrent) ? fileName : this.currentPath + '/' + splittedFileName[splittedFileName.length - 1]
this.helper.createNonClashingNameWithPrefix(fileNameToImport, fileProvider, '_test', (error: any, newFile: any) => { this.helper.createNonClashingNameWithPrefix(fileNameToImport, fileProvider, '_test', (error: Error, newFile: string) => {
if (error) return errorCb('Failed to create file. ' + newFile + ' ' + error) if (error) return errorCb('Failed to create file. ' + newFile + ' ' + error)
const isFileCreated = fileProvider.set(newFile, this.generateTestContractSample(hasCurrent, fileName)) const isFileCreated = fileProvider.set(newFile, this.generateTestContractSample(hasCurrent, fileName))
if (!isFileCreated) return errorCb('Failed to create test file ' + newFile) if (!isFileCreated) return errorCb('Failed to create test file ' + newFile)
@ -72,7 +74,7 @@ export class TestTabLogic {
let files = [] let files = []
try { try {
if (await this.fileManager.exists(this.currentPath)) files = await this.fileManager.readdir(this.currentPath) if (await this.fileManager.exists(this.currentPath)) files = await this.fileManager.readdir(this.currentPath)
} catch (e: any) { } catch (e: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
throw e.message throw e.message
} }
for (const file in files) { for (const file in files) {
@ -84,7 +86,7 @@ export class TestTabLogic {
// @todo(#2758): If currently selected file is compiled and compilation result is available, // @todo(#2758): If currently selected file is compiled and compilation result is available,
// 'contractName' should be <compiledContractName> + '_testSuite' // 'contractName' should be <compiledContractName> + '_testSuite'
generateTestContractSample (hasCurrent: any, fileToImport: any, contractName = 'testSuite') { generateTestContractSample (hasCurrent: boolean, fileToImport: string, contractName = 'testSuite') {
let relative = remixPath.relative(this.currentPath, remixPath.dirname(fileToImport)) let relative = remixPath.relative(this.currentPath, remixPath.dirname(fileToImport))
if (relative === '') relative = '.' if (relative === '') relative = '.'
const comment = hasCurrent ? `import "${relative}/${remixPath.basename(fileToImport)}";` : '// <import file to test>' const comment = hasCurrent ? `import "${relative}/${remixPath.basename(fileToImport)}";` : '// <import file to test>'

@ -1,62 +1,84 @@
import React, { useState, useRef, useEffect } from 'react' // eslint-disable-line import React, { useState, useRef, useEffect, ReactElement } from 'react' // eslint-disable-line
import { eachOfSeries } from 'async' // eslint-disable-line import { eachOfSeries } from 'async' // eslint-disable-line
import type Web3 from 'web3'
import { canUseWorker, urlFromVersion } from '@remix-project/remix-solidity' import { canUseWorker, urlFromVersion } from '@remix-project/remix-solidity'
import { Renderer } from '@remix-ui/renderer' // eslint-disable-line import { Renderer } from '@remix-ui/renderer' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import { format } from 'util' import { format } from 'util'
import './css/style.css' import './css/style.css'
const _paq = (window as any)._paq = (window as any)._paq || [] // eslint-disable-line const _paq = (window as any)._paq = (window as any)._paq || [] // eslint-disable-line @typescript-eslint/no-explicit-any
/* eslint-disable-next-line */
export interface SolidityUnitTestingProps { }
interface TestObject { interface TestObject {
fileName: string fileName: string
checked: boolean checked: boolean
} }
export const SolidityUnitTesting = (props: Record<string, any>) => { interface TestResultInterface {
type: string
value: any // eslint-disable-line @typescript-eslint/no-explicit-any
time?: number
context?: string
errMsg?: string
filename: string
assertMethod?: string
returned?: string | number
expected?: string | number
location?: string
hhLogs?: []
web3?: Web3
debugTxHash?: string
rendered?: boolean
}
interface FinalResult {
totalPassing: number,
totalFailing: number,
totalTime: any, // eslint-disable-line @typescript-eslint/no-explicit-any
errors: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
}
export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const { helper, testTab, initialPath } = props const { helper, testTab, initialPath } = props
const { testTabLogic } = testTab const { testTabLogic } = testTab
const [toasterMsg, setToasterMsg] = useState('') const [toasterMsg, setToasterMsg] = useState<string>('')
const [disableCreateButton, setDisableCreateButton] = useState(true) const [disableCreateButton, setDisableCreateButton] = useState<boolean>(true)
const [disableGenerateButton, setDisableGenerateButton] = useState(false) const [disableGenerateButton, setDisableGenerateButton] = useState<boolean>(false)
const [disableStopButton, setDisableStopButton] = useState(true) const [disableStopButton, setDisableStopButton] = useState<boolean>(true)
const [disableRunButton, setDisableRunButton] = useState(false) const [disableRunButton, setDisableRunButton] = useState<boolean>(false)
const [runButtonTitle, setRunButtonTitle] = useState('Run tests') const [runButtonTitle, setRunButtonTitle] = useState<string>('Run tests')
const [stopButtonLabel, setStopButtonLabel] = useState('Stop') const [stopButtonLabel, setStopButtonLabel] = useState<string>('Stop')
const [checkSelectAll, setCheckSelectAll] = useState(true) const [checkSelectAll, setCheckSelectAll] = useState<boolean>(true)
const [testsOutput, setTestsOutput] = useState<Element[]>([]) const [testsOutput, setTestsOutput] = useState<ReactElement[]>([])
const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState(true) const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState<boolean>(true)
const [progressBarHidden, setProgressBarHidden] = useState(true) const [progressBarHidden, setProgressBarHidden] = useState<boolean>(true)
const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState(true) const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState<boolean>(true)
let [testFiles, setTestFiles] = useState<TestObject[]>([]) // eslint-disable-line let [testFiles, setTestFiles] = useState<TestObject[]>([]) // eslint-disable-line
const [pathOptions, setPathOptions] = useState(['']) const [pathOptions, setPathOptions] = useState<string[]>([''])
const [inputPathValue, setInputPathValue] = useState('tests') const [inputPathValue, setInputPathValue] = useState<string>('tests')
let [readyTestsNumber, setReadyTestsNumber] = useState(0) // eslint-disable-line let [readyTestsNumber, setReadyTestsNumber] = useState<number>(0) // eslint-disable-line
let [runningTestsNumber, setRunningTestsNumber] = useState(0) // eslint-disable-line let [runningTestsNumber, setRunningTestsNumber] = useState<number>(0) // eslint-disable-line
const hasBeenStopped = useRef(false) const hasBeenStopped = useRef<boolean>(false)
const isDebugging = useRef(false) const isDebugging = useRef<boolean>(false)
const allTests: any = useRef([]) const allTests = useRef<string[]>([])
const selectedTests: any = useRef([]) const selectedTests = useRef<string[]>([])
const currentErrors: any = useRef([]) const currentErrors:any = useRef([]) // eslint-disable-line @typescript-eslint/no-explicit-any
const defaultPath = 'tests' const defaultPath = 'tests'
let areTestsRunning = false let areTestsRunning = false
let runningTestFileName: any let runningTestFileName: string
const filesContent: any = {} const filesContent: Record<string, Record<string, string>> = {}
const testsResultByFilename: Record<string, any> = {} const testsResultByFilename: Record<string, Record<string, Record<string, any>>> = {} // eslint-disable-line @typescript-eslint/no-explicit-any
const trimTestDirInput = (input: string) => { const trimTestDirInput = (input: string) => {
if (input.includes('/')) return input.split('/').map(e => e.trim()).join('/') if (input.includes('/')) return input.split('/').map(e => e.trim()).join('/')
@ -71,7 +93,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
setTestsExecutionStoppedErrorHidden(true) setTestsExecutionStoppedErrorHidden(true)
} }
const updateForNewCurrent = async (file = null) => { const updateForNewCurrent = async (file: string | null = null) => {
// Ensure that when someone clicks on compilation error and that opens a new file // Ensure that when someone clicks on compilation error and that opens a new file
// Test result, which is compilation error in this case, is not cleared // Test result, which is compilation error in this case, is not cleared
if (currentErrors.current) { if (currentErrors.current) {
@ -92,7 +114,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
selectedTests.current = [...allTests.current] selectedTests.current = [...allTests.current]
updateTestFileList() updateTestFileList()
if (!areTestsRunning) await updateRunAction(file) if (!areTestsRunning) await updateRunAction(file)
} catch (e: any) { } catch (e: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
console.log(e) console.log(e)
setToasterMsg(e) setToasterMsg(e)
} }
@ -132,17 +154,17 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
}) })
testTab.fileManager.events.on('noFileSelected', () => { }) // eslint-disable-line testTab.fileManager.events.on('noFileSelected', () => { }) // eslint-disable-line
testTab.fileManager.events.on('currentFileChanged', async (file: any, provider: any) => await updateForNewCurrent(file)) testTab.fileManager.events.on('currentFileChanged', async (file: string) => await updateForNewCurrent(file))
}, []) // eslint-disable-line }, []) // eslint-disable-line
const updateDirList = (path: string) => { const updateDirList = (path: string) => {
testTabLogic.dirList(path).then((options: any) => { testTabLogic.dirList(path).then((options: string[]) => {
setPathOptions(options) setPathOptions(options)
}) })
} }
const handleTestDirInput = async (e: any) => { const handleTestDirInput = async (e: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
let testDirInput = trimTestDirInput(e.target.value) let testDirInput = trimTestDirInput(e.target.value)
testDirInput = helper.removeMultipleSlashes(testDirInput) testDirInput = helper.removeMultipleSlashes(testDirInput)
if (testDirInput !== '/') testDirInput = helper.removeTrailingSlashes(testDirInput) if (testDirInput !== '/') testDirInput = helper.removeTrailingSlashes(testDirInput)
@ -183,7 +205,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} }
} }
const handleEnter = async (e: any) => { const handleEnter = async (e: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
let inputPath = e.target.value let inputPath = e.target.value
inputPath = helper.removeMultipleSlashes(trimTestDirInput(inputPath)) inputPath = helper.removeMultipleSlashes(trimTestDirInput(inputPath))
setInputPathValue(inputPath) setInputPathValue(inputPath)
@ -210,18 +232,18 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
setPathOptions(pathOptions) setPathOptions(pathOptions)
} }
const cleanFileName = (fileName: any, testSuite: any) => { const cleanFileName = (fileName: string, testSuite: string) => {
return fileName ? fileName.replace(/\//g, '_').replace(/\./g, '_') + testSuite : fileName return fileName ? fileName.replace(/\//g, '_').replace(/\./g, '_') + testSuite : fileName
} }
const startDebug = async (txHash: any, web3: any) => { const startDebug = async (txHash: string, web3: Web3) => {
isDebugging.current = true isDebugging.current = true
if (!await testTab.appManager.isActive('debugger')) await testTab.appManager.activatePlugin('debugger') if (!await testTab.appManager.isActive('debugger')) await testTab.appManager.activatePlugin('debugger')
testTab.call('menuicons', 'select', 'debugger') testTab.call('menuicons', 'select', 'debugger')
testTab.call('debugger', 'debug', txHash, web3) testTab.call('debugger', 'debug', txHash, web3)
} }
const printHHLogs = (logsArr: any, testName: any) => { const printHHLogs = (logsArr: Record<string, any>[], testName: string) => { // eslint-disable-line @typescript-eslint/no-explicit-any
let finalLogs = `<b>${testName}:</b>\n` let finalLogs = `<b>${testName}:</b>\n`
for (const log of logsArr) { for (const log of logsArr) {
let formattedLog let formattedLog
@ -262,9 +284,9 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} }
} }
const renderContract = (filename: any, contract: any, index: number, withoutLabel = false) => { const renderContract = (filename: string, contract: string|null, index: number, withoutLabel = false) => {
if (withoutLabel) { if (withoutLabel) {
const contractCard: any = ( const contractCard: ReactElement = (
<div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1"> <div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1">
<span className="font-weight-bold">{contract ? contract : ''} ({filename})</span> <span className="font-weight-bold">{contract ? contract : ''} ({filename})</span>
</div> </div>
@ -291,20 +313,20 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
</div>) </div>)
} }
// show contract and file name with label // show contract and file name with label
const ContractCard: any = ( const ContractCard: ReactElement = (
<div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1"> <div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1">
{label}<span className="font-weight-bold">{contract} ({filename})</span> {label}<span className="font-weight-bold">{contract} ({filename})</span>
</div> </div>
) )
setTestsOutput(prevCards => { setTestsOutput(prevCards => {
const index = prevCards.findIndex((card: any) => card.props.id === runningTestFileName) const index = prevCards.findIndex((card: ReactElement) => card.props.id === runningTestFileName)
prevCards[index] = ContractCard prevCards[index] = ContractCard
return prevCards return prevCards
}) })
} }
const renderTests = (tests: any, contract: any, filename: any) => { const renderTests = (tests: TestResultInterface[], contract: string, filename: string) => {
const index = tests.findIndex((test: any) => test.type === 'testFailure') const index = tests.findIndex((test: TestResultInterface) => test.type === 'testFailure')
// show filename and contract // show filename and contract
renderContract(filename, contract, index) renderContract(filename, contract, index)
// show tests // show tests
@ -321,7 +343,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} }
if (test.type === 'testPass') { if (test.type === 'testPass') {
if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value) if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
const testPassCard: any = ( const testPassCard: ReactElement = (
<div <div
id={runningTestFileName} id={runningTestFileName}
data-id="testTabSolidityUnitTestsOutputheader" data-id="testTabSolidityUnitTestsOutputheader"
@ -339,10 +361,10 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} else if (test.type === 'testFailure') { } else if (test.type === 'testFailure') {
if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value) if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
if (!test.assertMethod) { if (!test.assertMethod) {
const testFailCard1: any = (<div const testFailCard1: ReactElement = (<div
className="bg-light mb-2 px-2 testLog d-flex flex-column text-danger border-0" className="bg-light mb-2 px-2 testLog d-flex flex-column text-danger border-0"
id={"UTContext" + test.context} id={"UTContext" + test.context}
onClick={() => highlightLocation(test.location, test.filename)} onClick={() => { if(test.location) highlightLocation(test.location, test.filename)}}
> >
<div className="d-flex my-1 align-items-start justify-content-between"> <div className="d-flex my-1 align-items-start justify-content-between">
<span> {test.value}</span> <span> {test.value}</span>
@ -356,10 +378,10 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
const preposition = test.assertMethod === 'equal' || test.assertMethod === 'notEqual' ? 'to' : '' const preposition = test.assertMethod === 'equal' || test.assertMethod === 'notEqual' ? 'to' : ''
const method = test.assertMethod === 'ok' ? '' : test.assertMethod const method = test.assertMethod === 'ok' ? '' : test.assertMethod
const expected = test.assertMethod === 'ok' ? '\'true\'' : test.expected const expected = test.assertMethod === 'ok' ? '\'true\'' : test.expected
const testFailCard2: any = (<div const testFailCard2: ReactElement = (<div
className="bg-light mb-2 px-2 testLog d-flex flex-column text-danger border-0" className="bg-light mb-2 px-2 testLog d-flex flex-column text-danger border-0"
id={"UTContext" + test.context} id={"UTContext" + test.context}
onClick={() => highlightLocation(test.location, test.filename)} onClick={() => { if(test.location) highlightLocation(test.location, test.filename)}}
> >
<div className="d-flex my-1 align-items-start justify-content-between"> <div className="d-flex my-1 align-items-start justify-content-between">
<span> {test.value}</span> <span> {test.value}</span>
@ -396,7 +418,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
for (const contract of contracts) { for (const contract of contracts) {
if (contract && contract !== 'summary' && contract !== 'errors') { if (contract && contract !== 'summary' && contract !== 'errors') {
runningTestFileName = cleanFileName(filename, contract) runningTestFileName = cleanFileName(filename, contract)
const tests = fileTestsResult[contract] const tests = fileTestsResult[contract] as TestResultInterface[]
if (tests?.length) { if (tests?.length) {
renderTests(tests, contract, filename) renderTests(tests, contract, filename)
} else { } else {
@ -406,18 +428,18 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} else if (contract === 'errors' && fileTestsResult['errors']) { } else if (contract === 'errors' && fileTestsResult['errors']) {
const errors = fileTestsResult['errors'] const errors = fileTestsResult['errors']
if (errors && errors.errors) { if (errors && errors.errors) {
errors.errors.forEach((err: any) => { errors.errors.forEach((err: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const errorCard: any = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} /> const errorCard: ReactElement = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} />
setTestsOutput(prevCards => ([...prevCards, errorCard])) setTestsOutput(prevCards => ([...prevCards, errorCard]))
}) })
} else if (errors && Array.isArray(errors) && (errors[0].message || errors[0].formattedMessage)) { } else if (errors && Array.isArray(errors) && (errors[0].message || errors[0].formattedMessage)) {
errors.forEach((err) => { errors.forEach((err) => {
const errorCard: any = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} /> const errorCard: ReactElement = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} />
setTestsOutput(prevCards => ([...prevCards, errorCard])) setTestsOutput(prevCards => ([...prevCards, errorCard]))
}) })
} else if (errors && !errors.errors && !Array.isArray(errors)) { } else if (errors && !errors.errors && !Array.isArray(errors)) {
// To track error like this: https://github.com/ethereum/remix/pull/1438 // To track error like this: https://github.com/ethereum/remix/pull/1438
const errorCard: any = <Renderer message={errors.formattedMessage || errors.message} plugin={testTab} opt={{ type: 'error' }} /> const errorCard: ReactElement = <Renderer message={errors.formattedMessage || errors.message} plugin={testTab} opt={{ type: 'error' }} />
setTestsOutput(prevCards => ([...prevCards, errorCard])) setTestsOutput(prevCards => ([...prevCards, errorCard]))
} }
} }
@ -425,7 +447,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
// show summary // show summary
const testSummary = fileTestsResult['summary'] const testSummary = fileTestsResult['summary']
if (testSummary && testSummary.filename && !testSummary.rendered) { if (testSummary && testSummary.filename && !testSummary.rendered) {
const summaryCard: any = (<div className="d-flex alert-secondary mb-3 p-3 flex-column"> const summaryCard: ReactElement = (<div className="d-flex alert-secondary mb-3 p-3 flex-column">
<span className="font-weight-bold">Result for {testSummary.filename}</span> <span className="font-weight-bold">Result for {testSummary.filename}</span>
<span className="text-success">Passed: {testSummary.passed}</span> <span className="text-success">Passed: {testSummary.passed}</span>
<span className="text-danger">Failed: {testSummary.failed}</span> <span className="text-danger">Failed: {testSummary.failed}</span>
@ -437,7 +459,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} }
} }
const testCallback = (result: any) => { const testCallback = (result: Record<string, any>) => { // eslint-disable-line @typescript-eslint/no-explicit-any
if (result.filename) { if (result.filename) {
if (!testsResultByFilename[result.filename]) { if (!testsResultByFilename[result.filename]) {
testsResultByFilename[result.filename] = {} testsResultByFilename[result.filename] = {}
@ -455,7 +477,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} }
} }
const resultsCallback = (_err: any, result: any, cb: any) => { const resultsCallback = (_err: any, result: any, cb: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
// total stats for the test // total stats for the test
// result.passingNum // result.passingNum
// result.failureNum // result.failureNum
@ -463,7 +485,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
cb() cb()
} }
const updateFinalResult = (_errors: any, result: any, filename: any) => { const updateFinalResult = (_errors: any, result: FinalResult|null, filename: string) => { // eslint-disable-line @typescript-eslint/no-explicit-any
++readyTestsNumber ++readyTestsNumber
setReadyTestsNumber(readyTestsNumber) setReadyTestsNumber(readyTestsNumber)
if (!result && (_errors && (_errors.errors || (Array.isArray(_errors) && (_errors[0].message || _errors[0].formattedMessage))))) { if (!result && (_errors && (_errors.errors || (Array.isArray(_errors) && (_errors[0].message || _errors[0].formattedMessage))))) {
@ -500,14 +522,14 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} }
} }
const runTest = (testFilePath: any, callback: any) => { const runTest = (testFilePath: string, callback: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
isDebugging.current = false isDebugging.current = false
if (hasBeenStopped.current) { if (hasBeenStopped.current) {
updateFinalResult(null, null, null) updateFinalResult(null, null, testFilePath)
return return
} }
testTab.fileManager.readFile(testFilePath).then((content: any) => { testTab.fileManager.readFile(testFilePath).then((content: string) => {
const runningTests: any = {} const runningTests: Record<string, Record<string, string>> = {}
runningTests[testFilePath] = { content } runningTests[testFilePath] = { content }
filesContent[testFilePath] = { content } filesContent[testFilePath] = { content }
const { currentVersion, evmVersion, optimize, runs, isUrl } = testTab.compileTab.getCurrentCompilerConfig() const { currentVersion, evmVersion, optimize, runs, isUrl } = testTab.compileTab.getCurrentCompilerConfig()
@ -519,24 +541,24 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
usingWorker: canUseWorker(currentVersion), usingWorker: canUseWorker(currentVersion),
runs runs
} }
const deployCb = async (file: any, contractAddress: any) => { const deployCb = async (file: string, contractAddress: string) => {
const compilerData = await testTab.call('compilerArtefacts', 'getCompilerAbstract', file) const compilerData = await testTab.call('compilerArtefacts', 'getCompilerAbstract', file)
await testTab.call('compilerArtefacts', 'addResolvedContract', contractAddress, compilerData) await testTab.call('compilerArtefacts', 'addResolvedContract', contractAddress, compilerData)
} }
testTab.testRunner.runTestSources( testTab.testRunner.runTestSources(
runningTests, runningTests,
compilerConfig, compilerConfig,
(result: any) => testCallback(result), (result: Record<string, any>) => testCallback(result), // eslint-disable-line @typescript-eslint/no-explicit-any
(_err: any, result: any, cb: any) => resultsCallback(_err, result, cb), (_err: any, result: any, cb: any) => resultsCallback(_err, result, cb), // eslint-disable-line @typescript-eslint/no-explicit-any
deployCb, deployCb,
(error: any, result: any) => { (error: any, result: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
updateFinalResult(error, result, testFilePath) updateFinalResult(error, result, testFilePath)
callback(error) callback(error)
}, (url: any, cb: any) => { }, (url: string, cb: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
return testTab.contentImport.resolveAndSave(url).then((result: any) => cb(null, result)).catch((error: any) => cb(error.message)) return testTab.contentImport.resolveAndSave(url).then((result: any) => cb(null, result)).catch((error: Error) => cb(error.message)) // eslint-disable-line @typescript-eslint/no-explicit-any
}, { testFilePath } }, { testFilePath }
) )
}).catch((error: any) => { }).catch((error: Error) => {
console.log(error) console.log(error)
if (error) return // eslint-disable-line if (error) return // eslint-disable-line
}) })
@ -552,17 +574,17 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
setDisableStopButton(false) setDisableStopButton(false)
clearResults() clearResults()
setProgressBarHidden(false) setProgressBarHidden(false)
const tests = selectedTests.current const tests: string[] = selectedTests.current
if (!tests || !tests.length) return if (!tests || !tests.length) return
else setProgressBarHidden(false) else setProgressBarHidden(false)
_paq.push(['trackEvent', 'solidityUnitTesting', 'runTests']) _paq.push(['trackEvent', 'solidityUnitTesting', 'runTests'])
eachOfSeries(tests, (value: any, key: any, callback: any) => { eachOfSeries(tests, (value: string, key: string, callback: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
if (hasBeenStopped.current) return if (hasBeenStopped.current) return
runTest(value, callback) runTest(value, callback)
}) })
} }
const updateRunAction = async (currentFile: any = null) => { const updateRunAction = async (currentFile: any = null) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const isSolidityActive = await testTab.appManager.isActive('solidity') const isSolidityActive = await testTab.appManager.isActive('solidity')
if (!isSolidityActive || !selectedTests.current?.length) { if (!isSolidityActive || !selectedTests.current?.length) {
// setDisableRunButton(true) // setDisableRunButton(true)
@ -586,7 +608,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
return selectedTestsList.map(testFileObj => testFileObj.fileName) return selectedTestsList.map(testFileObj => testFileObj.fileName)
} }
const toggleCheckbox = (eChecked: any, index: any) => { const toggleCheckbox = (eChecked: boolean, index: number) => {
testFiles[index].checked = eChecked testFiles[index].checked = eChecked
setTestFiles(testFiles) setTestFiles(testFiles)
selectedTests.current = getCurrentSelectedTests() selectedTests.current = getCurrentSelectedTests()
@ -603,7 +625,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} else setCheckSelectAll(false) } else setCheckSelectAll(false)
} }
const checkAll = (event: any) => { const checkAll = (event: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
testFiles.forEach((testFileObj) => testFileObj.checked = event.target.checked) testFiles.forEach((testFileObj) => testFileObj.checked = event.target.checked)
setTestFiles(testFiles) setTestFiles(testFiles)
setCheckSelectAll(event.target.checked) setCheckSelectAll(event.target.checked)
@ -618,7 +640,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
const updateTestFileList = () => { const updateTestFileList = () => {
if (allTests.current?.length) { if (allTests.current?.length) {
testFiles = allTests.current.map((testFile: any) => { return { 'fileName': testFile, 'checked': true } }) testFiles = allTests.current.map((testFile: string) => { return { 'fileName': testFile, 'checked': true } })
setCheckSelectAll(true) setCheckSelectAll(true)
} }
else else
@ -674,7 +696,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
title="Generate sample test file." title="Generate sample test file."
disabled={disableGenerateButton} disabled={disableGenerateButton}
onClick={async () => { onClick={async () => {
testTabLogic.generateTestFile((err:any) => { if (err) setToasterMsg(err)}) testTabLogic.generateTestFile((err:any) => { if (err) setToasterMsg(err)}) // eslint-disable-line @typescript-eslint/no-explicit-any
await updateForNewCurrent() await updateForNewCurrent()
}} }}
> >
@ -704,7 +726,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
/> />
<label className="text-nowrap pl-2 mb-0" htmlFor="checkAllTests"> Select all </label> <label className="text-nowrap pl-2 mb-0" htmlFor="checkAllTests"> Select all </label>
</div> </div>
<div className="testList py-2 mt-0 border-bottom">{testFiles?.length ? testFiles.map((testFileObj: any, index) => { <div className="testList py-2 mt-0 border-bottom">{testFiles?.length ? testFiles.map((testFileObj: TestObject, index) => {
const elemId = `singleTest${testFileObj.fileName}` const elemId = `singleTest${testFileObj.fileName}`
return ( return (
<div className="d-flex align-items-center py-1" key={index}> <div className="d-flex align-items-center py-1" key={index}>

@ -106,8 +106,8 @@ export const registerErrorScriptRunnerAction = (on, commandName, commandFn, disp
}) })
} }
export const listenOnNetworkAction = async (event, isListening) => { export const listenOnNetworkAction = async (plugins, isListening) => {
event.trigger('listenOnNetWork', [isListening]) plugins.txListener.setListenOnNetwork(isListening)
} }
export const initListeningOnNetwork = (plugins, dispatch: React.Dispatch<any>) => { export const initListeningOnNetwork = (plugins, dispatch: React.Dispatch<any>) => {

@ -1,77 +0,0 @@
import React, { useEffect, useState } from 'react'
export const useDragTerminal = (minHeight: number, defaultPosition: number) => {
const [isOpen, setIsOpen] = useState(defaultPosition > minHeight)
const [lastYPosition, setLastYPosition] = useState(0)
const [terminalPosition, setTerminalPosition] = useState(defaultPosition)
// Used to save position of the terminal when it is closed
const [lastTerminalPosition, setLastTerminalPosition] = useState(defaultPosition)
const [isDragging, setIsDragging] = useState(false)
const handleDraggingStart = (event: React.MouseEvent) => {
setLastYPosition(event.clientY)
setIsDragging(true)
}
const handleDragging = (event: MouseEvent) => {
event.preventDefault()
if (isDragging) {
const mouseYPosition = event.clientY
const difference = lastYPosition - mouseYPosition
const newTerminalPosition = terminalPosition + difference
setTerminalPosition(newTerminalPosition)
setLastYPosition(mouseYPosition)
}
}
const handleDraggingEnd = () => {
if (!isDragging) return
setIsDragging(false)
// Check terminal position to determine if it should be open or closed
setIsOpen(terminalPosition > minHeight)
}
const handleToggleTerminal = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault()
event.stopPropagation()
if (isOpen) {
setLastTerminalPosition(terminalPosition)
setLastYPosition(0)
setTerminalPosition(minHeight)
} else {
setTerminalPosition(lastTerminalPosition <= minHeight ? 323 : lastTerminalPosition)
}
setIsOpen(!isOpen)
}
// Add event listeners for dragging
useEffect(() => {
document.addEventListener('mousemove', handleDragging)
document.addEventListener('mouseup', handleDraggingEnd)
return () => {
document.removeEventListener('mousemove', handleDragging)
document.removeEventListener('mouseup', handleDraggingEnd)
}
}, [handleDragging, handleDraggingEnd])
// Reset terminal position
useEffect(() => {
if (!terminalPosition) {
setTerminalPosition(defaultPosition)
}
}, [terminalPosition, setTerminalPosition])
return {
isOpen,
terminalPosition,
isDragging,
handleDraggingStart,
handleToggleTerminal
}
}

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState, useEffect, useReducer, useRef, SyntheticEvent, MouseEvent } from 'react' // eslint-disable-line import React, { useState, useEffect, useReducer, useRef, SyntheticEvent, MouseEvent } from 'react' // eslint-disable-line
import { registerCommandAction, registerLogScriptRunnerAction, registerInfoScriptRunnerAction, registerErrorScriptRunnerAction, registerWarnScriptRunnerAction, listenOnNetworkAction, initListeningOnNetwork } from './actions/terminalAction' import { registerCommandAction, registerLogScriptRunnerAction, registerInfoScriptRunnerAction, registerErrorScriptRunnerAction, registerWarnScriptRunnerAction, listenOnNetworkAction, initListeningOnNetwork } from './actions/terminalAction'
import { initialState, registerCommandReducer, addCommandHistoryReducer, registerScriptRunnerReducer } from './reducers/terminalReducer' import { initialState, registerCommandReducer, addCommandHistoryReducer, registerScriptRunnerReducer } from './reducers/terminalReducer'
@ -17,7 +18,6 @@ import RenderKnownTransactions from './components/RenderKnownTransactions' // es
import parse from 'html-react-parser' import parse from 'html-react-parser'
import { EMPTY_BLOCK, KNOWN_TRANSACTION, RemixUiTerminalProps, UNKNOWN_TRANSACTION } from './types/terminalTypes' import { EMPTY_BLOCK, KNOWN_TRANSACTION, RemixUiTerminalProps, UNKNOWN_TRANSACTION } from './types/terminalTypes'
import { wrapScript } from './utils/wrapScript' import { wrapScript } from './utils/wrapScript'
import { useDragTerminal } from './custom-hooks/useDragTerminal'
/* eslint-disable-next-line */ /* eslint-disable-next-line */
export interface ClipboardEvent<T = Element> extends SyntheticEvent<T, any> { export interface ClipboardEvent<T = Element> extends SyntheticEvent<T, any> {
@ -25,10 +25,10 @@ export interface ClipboardEvent<T = Element> extends SyntheticEvent<T, any> {
} }
export const RemixUiTerminal = (props: RemixUiTerminalProps) => { export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
const { call, _deps, on, config, event, gistHandler, version } = props.plugin const { call, _deps, on, config, event, version } = props.plugin
const [_cmdIndex, setCmdIndex] = useState(-1) const [_cmdIndex, setCmdIndex] = useState(-1)
const [_cmdTemp, setCmdTemp] = useState('') const [_cmdTemp, setCmdTemp] = useState('')
const [isOpen, setIsOpen] = useState<boolean>(true)
const [newstate, dispatch] = useReducer(registerCommandReducer, initialState) const [newstate, dispatch] = useReducer(registerCommandReducer, initialState)
const [cmdHistory, cmdHistoryDispatch] = useReducer(addCommandHistoryReducer, initialState) const [cmdHistory, cmdHistoryDispatch] = useReducer(addCommandHistoryReducer, initialState)
const [, scriptRunnerDispatch] = useReducer(registerScriptRunnerReducer, initialState) const [, scriptRunnerDispatch] = useReducer(registerScriptRunnerReducer, initialState)
@ -79,24 +79,6 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
const terminalMenuOffsetHeight = (terminalMenu.current && terminalMenu.current.offsetHeight) || 35 const terminalMenuOffsetHeight = (terminalMenu.current && terminalMenu.current.offsetHeight) || 35
const terminalDefaultPosition = config.get('terminal-top-offset') const terminalDefaultPosition = config.get('terminal-top-offset')
const {
isOpen,
isDragging,
terminalPosition,
handleDraggingStart,
handleToggleTerminal
} = useDragTerminal(terminalMenuOffsetHeight, terminalDefaultPosition)
// Check open state
useEffect(() => {
const resizeValue = isOpen ? [config.get('terminal-top-offset')] : []
event.trigger('resize', resizeValue)
}, [isOpen])
useEffect(() => {
event.trigger('resize', [terminalPosition])
}, [terminalPosition])
const scrollToBottom = () => { const scrollToBottom = () => {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
} }
@ -106,6 +88,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
logHtml: (html) => { logHtml: (html) => {
scriptRunnerDispatch({ type: 'html', payload: { message: [html.innerText] } }) scriptRunnerDispatch({ type: 'html', payload: { message: [html.innerText] } })
}, },
log: (message) => { log: (message) => {
scriptRunnerDispatch({ type: 'log', payload: { message: [message] } }) scriptRunnerDispatch({ type: 'log', payload: { message: [message] } })
} }
@ -178,7 +161,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
} }
function loadgist (id, cb) { function loadgist (id, cb) {
gistHandler.loadFromGist({ gist: id }, _deps.fileManager) props.plugin.call('gistHandler', 'load', id)
if (cb) cb() if (cb) cb()
} }
@ -332,8 +315,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
const listenOnNetwork = (e: any) => { const listenOnNetwork = (e: any) => {
const isListening = e.target.checked const isListening = e.target.checked
// setIsListeningOnNetwork(isListening) listenOnNetworkAction(props.plugin, isListening)
listenOnNetworkAction(event, isListening)
} }
const onChange = (event: any) => { const onChange = (event: any) => {
@ -426,10 +408,14 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: 0, showSuggestions: false })) setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: 0, showSuggestions: false }))
} }
const handleToggleTerminal = () => {
setIsOpen(!isOpen)
props.plugin.call('layout', 'minimize', props.plugin.profile.name, isOpen)
}
return ( return (
<div style={{ height: '323px', flexGrow: 1 }} className='panel' ref={panelRef}> <div style={{ flexGrow: 1 }} className='panel' ref={panelRef}>
<div className="bar"> <div className="bar">
<div className={`dragbarHorizontal ${isDragging ? 'dragbarDragging' : ''}`} onMouseDown={handleDraggingStart} ref={leftRef}></div>
<div className="menu border-top border-dark bg-light" ref={terminalMenu} data-id="terminalToggleMenu"> <div className="menu border-top border-dark bg-light" ref={terminalMenu} data-id="terminalToggleMenu">
<i className={`mx-2 toggleTerminal fas ${isOpen ? 'fa-angle-double-down' : 'fa-angle-double-up'}`} data-id="terminalToggleIcon" onClick={handleToggleTerminal}></i> <i className={`mx-2 toggleTerminal fas ${isOpen ? 'fa-angle-double-down' : 'fa-angle-double-up'}`} data-id="terminalToggleIcon" onClick={handleToggleTerminal}></i>
<div className="mx-2 console" id="clearConsole" data-id="terminalClearConsole" onClick={handleClearConsole} > <div className="mx-2 console" id="clearConsole" data-id="terminalClearConsole" onClick={handleClearConsole} >

@ -117,11 +117,7 @@ export const loadWorkspacePreset = async (template: 'gist-template' | 'code-temp
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element] obj['/' + 'gist-' + gistId + '/' + path] = data.files[element]
}) })
plugin.fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => { plugin.fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => {
if (!errorLoadingFile) { if (errorLoadingFile) {
const provider = plugin.fileManager.getProvider('workspace')
provider.lastLoadedGistId = gistId
} else {
dispatch(displayNotification('', errorLoadingFile.message || errorLoadingFile, 'OK', null, () => {}, null)) dispatch(displayNotification('', errorLoadingFile.message || errorLoadingFile, 'OK', null, () => {}, null))
} }
}) })

@ -672,9 +672,11 @@ const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: s
return files return files
} }
} else { } else {
if (payload.path === state.mode || payload.path === '/') { if (payload.path === '/') {
const files = normalize(payload.fileTree, payload.path, payload.type)
return { [state.mode]: files }
} else if (payload.path === state.mode) {
let files = normalize(payload.fileTree, payload.path, payload.type) let files = normalize(payload.fileTree, payload.path, payload.type)
files = _.merge(files, state[state.mode].files[state.mode]) files = _.merge(files, state[state.mode].files[state.mode])
if (deletePath) delete files[deletePath] if (deletePath) delete files[deletePath]
return { [state.mode]: files } return { [state.mode]: files }

@ -145,6 +145,9 @@
"remix-ui-tabs": { "remix-ui-tabs": {
"tags": [] "tags": []
}, },
"remix-ui-panel": {
"tags": []
},
"remix-ui-theme-module": { "remix-ui-theme-module": {
"tags": [] "tags": []
}, },

@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic", "workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph", "dep-graph": "nx dep-graph",
"help": "nx help", "help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor,remix-ui-app,remix-ui-tabs", "lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,solidity-unit-testing,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor,remix-ui-app,remix-ui-tabs,remix-ui-panel",
"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", "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", "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", "publish:libs": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs",
@ -162,7 +162,7 @@
"chokidar": "^2.1.8", "chokidar": "^2.1.8",
"color-support": "^1.1.3", "color-support": "^1.1.3",
"commander": "^2.20.3", "commander": "^2.20.3",
"core-js": "^3.19.3", "core-js": "^3.6.5",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"document-register-element": "1.13.1", "document-register-element": "1.13.1",
"ethereumjs-util": "^7.0.10", "ethereumjs-util": "^7.0.10",

@ -69,8 +69,11 @@
"@remix-ui/tabs": ["libs/remix-ui/tabs/src/index.ts"], "@remix-ui/tabs": ["libs/remix-ui/tabs/src/index.ts"],
"@remix-ui/helper": ["libs/remix-ui/helper/src/index.ts"], "@remix-ui/helper": ["libs/remix-ui/helper/src/index.ts"],
"@remix-ui/app": ["libs/remix-ui/app/src/index.ts"], "@remix-ui/app": ["libs/remix-ui/app/src/index.ts"],
"@remix-ui/vertical-icons-panel": ["libs/remix-ui/vertical-icons-panel/src/index.ts"], "@remix-ui/vertical-icons-panel": [
"libs/remix-ui/vertical-icons-panel/src/index.ts"
],
"@remix-ui/theme-module": ["libs/remix-ui/theme-module/src/index.ts"], "@remix-ui/theme-module": ["libs/remix-ui/theme-module/src/index.ts"],
"@remix-ui/panel": ["libs/remix-ui/panel/src/index.ts"],
"@remix-ui/editor-context-view": ["libs/remix-ui/editor-context-view/src/index.ts"], "@remix-ui/editor-context-view": ["libs/remix-ui/editor-context-view/src/index.ts"],
"@remix-ui/solidity-unit-testing": [ "@remix-ui/solidity-unit-testing": [
"libs/remix-ui/solidity-unit-testing/src/index.ts" "libs/remix-ui/solidity-unit-testing/src/index.ts"

@ -82,16 +82,6 @@
"apps/remix-ide/src/assets/js/**/*.js" "apps/remix-ide/src/assets/js/**/*.js"
] ]
} }
},
"test": {
"builder": "@nrwl/workspace:run-commands",
"options": {
"commands": [
{
"command": "csslint && node apps/remix-ide/test/index.js"
}
]
}
} }
} }
}, },
@ -1117,6 +1107,21 @@
} }
} }
}, },
"remix-ui-panel": {
"root": "libs/remix-ui/panel",
"sourceRoot": "libs/remix-ui/panel/src",
"projectType": "library",
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/panel/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/panel/**/*"]
}
}
}
},
"solidity-unit-testing": { "solidity-unit-testing": {
"root": "libs/remix-ui/solidity-unit-testing", "root": "libs/remix-ui/solidity-unit-testing",
"sourceRoot": "libs/remix-ui/solidity-unit-testing/src", "sourceRoot": "libs/remix-ui/solidity-unit-testing/src",

Loading…
Cancel
Save