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: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true
acceptSslCerts: true,
'moz:firefoxOptions': {
args: [
'-width=2560',
'-height=1440'
]
}
}
},
@ -78,7 +84,11 @@ module.exports = {
javascriptEnabled: true,
acceptSslCerts: true,
'moz:firefoxOptions': {
args: ['-headless']
args: [
'-headless',
'-width=2560',
'-height=1440'
]
}
}
}

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

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

@ -76,29 +76,34 @@ module.exports = {
.waitForElementVisible('button[data-id="landingPageImportFromGistButton"]')
.pause(1000)
.scrollAndClick('button[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogModalTitle"]')
.assert.containsText('*[data-id="modalDialogModalTitle"]', 'Load a Gist')
.waitForElementVisible('*[data-id="modalDialogModalBody"]')
.assert.containsText('*[data-id="modalDialogModalBody"]', 'Enter the ID of the Gist or URL you would like to load.')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.modalFooterCancelClick()
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalTitle-react"]')
.assert.containsText('*[data-id="gisthandlerModalDialogModalTitle-react"]', 'Load a Gist')
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"]')
.assert.containsText('*[data-id="gisthandlerModalDialogModalBody-react"]', 'Enter the ID of the Gist or URL you would like to load.')
.waitForElementVisible('*[data-id="modalDialogCustomPromp"]')
.modalFooterCancelClick('gisthandler')
},
'Display Error Message For Invalid Gist ID': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', testData.invalidGistId)
.modalFooterOKClick()
.waitForElementVisible('*[data-id="modalDialogModalBody"]')
.assert.containsText('*[data-id="modalDialogModalBody"]', 'Not Found')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]')
.execute(() => {
(document.querySelector('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]') as any).focus()
}, [], () => {})
.setValue('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]', testData.invalidGistId)
.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) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.waitForElementVisible('[data-id="settingsTabRemoveGistToken"]')
@ -129,9 +134,12 @@ module.exports = {
.click('[data-id="settingsTabSaveGistToken"]')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', testData.validGistId)
.modalFooterOKClick()
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]')
.execute(() => {
(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`)
.waitForElementVisible(`div[title='default_workspace/gist-${testData.validGistId}/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) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]')
.execute(() => {
@ -48,8 +46,6 @@ module.exports = {
'Import From Github For Valid URL': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]')
.clearValue('*[data-id="homeTabModalDialogCustomPromptText"]')

@ -125,9 +125,9 @@ function startRemixd (browser: NightwatchBrowser) {
.clickLaunchIcon('filePanel')
.clickLaunchIcon('pluginManager')
.scrollAndClick('#pluginManager *[data-id="pluginManagerComponentActivateButtonremixd"]')
.waitForElementVisible('#modal-footer-ok', 2000)
.waitForElementVisible('*[data-id="remixdConnect-modal-footer-ok-react"]', 2000)
.pause(2000)
.click('#modal-footer-ok')
.click('*[data-id="remixdConnect-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,
checkVariableDebug(id: string, debugValue: NightwatchCheckVariableDebugValue): NightwatchBrowser,
addAtAddressInstance(address: string, isValidFormat: boolean, isValidChecksum: boolean): NightwatchBrowser,
modalFooterOKClick(): NightwatchBrowser,
modalFooterOKClick(id?: string): NightwatchBrowser,
clickInstance(index: number): NightwatchBrowser,
journalLastChildIncludes(val: string): NightwatchBrowser,
executeScript(script: string): NightwatchBrowser,
@ -32,7 +32,7 @@ declare module 'nightwatch' {
scrollToLine(line: number): NightwatchBrowser,
waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser,
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser,
modalFooterCancelClick(): NightwatchBrowser,
modalFooterCancelClick(id?: string): NightwatchBrowser,
selectContract(contractName: string): NightwatchBrowser,
createContract(inputParams: string): NightwatchBrowser,
getAddressAtPosition(index: number, cb: (pos: string) => void): NightwatchBrowser,

@ -2,7 +2,6 @@
import { RunTab, makeUdapp } from './app/udapp'
import { RemixEngine } from './remixEngine'
import { RemixAppManager } from './remixAppManager'
import { MainView } from './app/panels/main-view'
import { ThemeModule } from './app/tabs/theme-module'
import { NetworkModule } from './app/tabs/network-module'
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 { LandingPage } from './app/ui/landing-page/landing-page'
import { MainPanel } from './app/components/main-panel'
import { FramingService } from './framingService'
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 Registry from './app/state/registry'
import { ConfigPlugin } from './app/plugins/config'
import { Layout } from './app/panels/layout'
import { ModalPlugin } from './app/plugins/modal'
const isElectron = require('is-electron')
@ -49,6 +49,7 @@ const TestTab = require('./app/tabs/test-tab')
const FilePanel = require('./app/panels/file-panel')
const Editor = require('./app/editor/editor')
const Terminal = require('./app/panels/terminal')
const { TabProxy } = require('./app/panels/tab-proxy.js')
class AppComponent {
constructor () {
@ -66,13 +67,27 @@ class AppComponent {
// load file system
self._components.filesProviders = {}
self._components.filesProviders.browser = new FileProvider('browser')
Registry.getInstance().put({ api: self._components.filesProviders.browser, name: 'fileproviders/browser' })
self._components.filesProviders.localhost = new RemixDProvider(self.appManager)
Registry.getInstance().put({ api: self._components.filesProviders.localhost, name: 'fileproviders/localhost' })
Registry.getInstance().put({
api: self._components.filesProviders.browser,
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()
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)
}
@ -82,6 +97,7 @@ class AppComponent {
// APP_MANAGER
const appManager = self.appManager
const pluginLoader = self.appManager.pluginLoader
self.panels = {}
self.workspace = pluginLoader.get()
self.engine = new RemixEngine()
self.engine.register(appManager)
@ -91,8 +107,15 @@ class AppComponent {
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23
}
self.showMatamo = (matomoDomains[window.location.hostname] && !Registry.getInstance().get('config').api.exists('settings/matomo-analytics'))
self.walkthroughService = new WalkthroughService(appManager, self.showMatamo)
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']
// workaround for Electron support
@ -104,6 +127,8 @@ class AppComponent {
}
// SERVICES
// ----------------- gist service ---------------------------------
self.gistHandler = new GistHandler()
// ----------------- theme service ---------------------------------
self.themeModule = new ThemeModule()
Registry.getInstance().put({ api: self.themeModule, name: 'themeModule' })
@ -111,7 +136,9 @@ class AppComponent {
// ----------------- editor service ----------------------------
const editor = new Editor() // wrapper around ace editor
Registry.getInstance().put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', () => fileManager.saveCurrentFile())
editor.event.register('requiringToSaveCurrentfile', () =>
fileManager.saveCurrentFile()
)
// ----------------- fileManager service ----------------------------
const fileManager = new FileManager(editor, appManager)
@ -128,7 +155,10 @@ class AppComponent {
const compilerMetadataGenerator = new CompilerMetadata()
// ----------------- compilation result service (can keep track of compilation results) ----------------------------
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
const fetchAndCompile = new FetchAndCompile()
@ -139,28 +169,37 @@ class AppComponent {
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
Registry.getInstance().put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' })
Registry.getInstance().put({
api: offsetToLineColumnConverter,
name: 'offsettolinecolumnconverter'
})
// -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl))
makeUdapp(blockchain, compilersArtefacts, domEl => terminal.logHtml(domEl))
const terminal = new Terminal(
{ appManager, blockchain },
{
getPosition: (event) => {
getPosition: event => {
const limitUp = 36
const limitDown = 20
const height = window.innerHeight
let newpos = (event.pageY < limitUp) ? limitUp : event.pageY
newpos = (newpos < height - limitDown) ? newpos : height - limitDown
let newpos = event.pageY < limitUp ? limitUp : event.pageY
newpos = newpos < height - limitDown ? newpos : height - limitDown
return height - newpos
}
}
)
const contextualListener = new EditorContextListener()
self.modal = new ModalPlugin()
const configPlugin = new ConfigPlugin()
self.layout = new Layout()
self.engine.register([
self.layout,
self.modal,
self.gistHandler,
configPlugin,
blockchain,
contentImport,
@ -182,22 +221,27 @@ class AppComponent {
// LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel()
self.mainview = new MainView(contextualListener, editor, appPanel, fileManager, appManager, terminal)
Registry.getInstance().put({ api: self.mainview, name: 'mainview' })
self.engine.register([
appPanel,
self.mainview.tabProxy
])
const tabProxy = new TabProxy(fileManager, editor)
self.engine.register([appPanel, tabProxy])
// those views depend on app_manager
self.menuicons = new VerticalIcons(appManager)
self.sidePanel = new SidePanel(appManager, self.menuicons)
self.hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(appManager, self.engine)
const pluginManagerComponent = new PluginManagerComponent(
appManager,
self.engine
)
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(
Registry.getInstance().get('config').api,
editor,
@ -215,7 +259,10 @@ class AppComponent {
])
// 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(
blockchain,
Registry.getInstance().get('config').api,
@ -224,7 +271,6 @@ class AppComponent {
filePanel,
Registry.getInstance().get('compilersartefacts').api,
networkModule,
self.mainview,
Registry.getInstance().get('fileproviders/browser').api
)
const analysis = new AnalysisTab()
@ -249,6 +295,13 @@ class AppComponent {
filePanel.hardhatHandle,
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 () {
@ -263,61 +316,70 @@ class AppComponent {
try {
self.engine.register(await self.appManager.registeredPlugins())
} 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(['theme', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await self.appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs'])
await self.appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await self.appManager.activatePlugin(['home'])
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(['walkthrough'])
self.appManager.on('filePanel', 'workspaceInitializationCompleted', async () => {
await self.appManager.registerContextMenuItems()
})
self.appManager.on(
'filePanel',
'workspaceInitializationCompleted',
async () => {
await self.appManager.registerContextMenuItems()
}
)
await self.appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation
self.appManager.on('editor', 'editorMounted', () => {
if (Array.isArray(self.workspace)) {
self.appManager.activatePlugin(self.workspace).then(async () => {
try {
if (params.deactivate) {
await self.appManager.deactivatePlugin(params.deactivate.split(','))
self.appManager
.activatePlugin(self.workspace)
.then(async () => {
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.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])
}
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)
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
self.appManager.activatePlugin(['solidity', 'udapp'])
// 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',
description: '',
version: packageJson.version,
methods: ['select']
methods: ['select', 'unlinkContent']
}
// 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 * as packageJson from '../../../../../package.json'
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 modalDialogCustom = require('../ui/modal-dialog-custom')
const helper = require('../../lib/helper.js')
/*
@ -22,7 +22,7 @@ const profile = {
icon: 'assets/img/fileManager.webp',
permission: true,
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'
}
const errorMsg = {
@ -37,6 +37,18 @@ const createError = (err) => {
}
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) {
super(profile)
this.mode = 'browser'
@ -70,7 +82,7 @@ class FileManager extends Plugin {
* @param {string} path path of the file/directory
* @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)
if (!exists) {
@ -96,7 +108,7 @@ class FileManager extends Plugin {
* @param {string} path path of the file/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)
if (!isDir) {
@ -305,13 +317,19 @@ class FileManager extends Plugin {
if (isFile) {
if (newPathExists) {
modalDialogCustom.alert('File already exists.')
this.call('modal', 'alert', {
id: 'fileManagerAlert',
message: 'File already exists'
})
return
}
return provider.rename(oldPath, newPath, false)
} else {
if (newPathExists) {
modalDialogCustom.alert('Folder already exists.')
this.call('modal', 'alert', {
id: 'fileManagerAlert',
message: 'Directory already exists'
})
return
}
return provider.rename(oldPath, newPath, true)
@ -612,7 +630,7 @@ class FileManager extends Plugin {
this.events.emit('noFileSelected')
}
async openFile (file) {
async openFile (file?: string) {
if (!file) {
this.emit('noFileSelected')
this.events.emit('noFileSelected')
@ -639,7 +657,7 @@ class FileManager extends Plugin {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('currentFileChanged', file)
this.events.emit('currentFileChanged', file)
resolve()
resolve(true)
}
})
})
@ -698,7 +716,7 @@ class FileManager extends Plugin {
dirPaths.push(item)
resolve(dirPaths)
}
return new Promise((resolve, reject) => { resolve() })
return new Promise((resolve, reject) => { resolve(true) })
})
Promise.all(promises).then(() => { resolve(dirPaths) })
})
@ -760,9 +778,15 @@ class FileManager extends Plugin {
helper.createNonClashingName(file, self._deps.filesProviders[fileProvider],
(error, name) => {
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)) {
modalDialogCustom.alert('Special characters are not allowed')
this.call('modal', 'alert', {
id: 'fileManagerAlert',
message: 'Special characters are not allowed in file names.'
})
} else {
try {
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 { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
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 { HardhatHandle } = require('../files/hardhat-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._handlers = {}
this.loadedTabs = []
this.el = document.createElement('div')
}
onActivation () {
@ -72,10 +73,12 @@ export class TabProxy extends Plugin {
this.addTab(workspacePath, '', () => {
this.fileManager.open(file)
this.event.emit('openFile', file)
this.emit('openFile', file)
},
() => {
this.fileManager.closeFile(file)
this.event.emit('closeFile', file)
this.emit('closeFile', file)
})
this.tabsApi.activateTab(workspacePath)
} else {
@ -88,10 +91,12 @@ export class TabProxy extends Plugin {
this.addTab(path, '', () => {
this.fileManager.open(file)
this.event.emit('openFile', file)
this.emit('openFile', file)
},
() => {
this.fileManager.closeFile(file)
this.event.emit('closeFile', file)
this.emit('closeFile', file)
})
this.tabsApi.activateTab(path)
}
@ -132,9 +137,9 @@ export class TabProxy extends Plugin {
this.addTab(
name,
displayName,
() => this.event.emit('switchApp', name),
() => this.emit('switchApp', name),
() => {
this.event.emit('closeApp', name)
this.emit('closeApp', name)
this.call('manager', 'deactivatePlugin', name)
},
icon
@ -149,7 +154,7 @@ export class TabProxy extends Plugin {
}
focus (name) {
this.event.emit('switchApp', name)
this.emit('switchApp', name)
this.tabsApi.activateTab(name)
}
@ -199,6 +204,7 @@ export class TabProxy extends Plugin {
() => {
this.fileManager.closeFile(newName)
this.event.emit('closeFile', newName)
this.emit('closeFile', newName)
})
this.removeTab(oldName)
}
@ -285,7 +291,7 @@ export class TabProxy extends Plugin {
if (this.loadedTabs[index]) {
const name = this.loadedTabs[index].name
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]) {
const name = this.loadedTabs[index].name
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 () {
this.el = document.createElement('div')
this.renderComponent()
return this.el
}
}

@ -13,8 +13,6 @@ const AutoCompletePopup = require('../ui/auto-complete-popup')
import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line
const GistHandler = require('../../lib/gist-handler')
const KONSOLES = []
function register (api) { KONSOLES.push(api) }
@ -22,7 +20,7 @@ function register (api) { KONSOLES.push(api) }
const profile = {
displayName: 'Terminal',
name: 'terminal',
methods: ['log'],
methods: ['log', 'logHtml'],
events: [],
description: ' - ',
version: packageJson.version
@ -32,7 +30,6 @@ class Terminal extends Plugin {
constructor (opts, api) {
super(profile)
this.fileImport = new CompilerImports()
this.gistHandler = new GistHandler()
this.event = new EventManager()
this.globalRegistry = Registry.getInstance()
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 { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json'
import { version as remixdVersion } from '../../../../../libs/remixd/package.json'
var yo = require('yo-yo')
var modalDialog = require('../ui/modaldialog')
var modalDialogCustom = require('../ui/modal-dialog-custom')
var copyToClipboard = require('../ui/copy-to-clipboard')
import { PluginManager } from '@remixproject/engine'
import { AppModal, AlertModal } from '@remix-ui/app'
import { CopyToClipboard } from '@remix-ui/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 profile = {
@ -32,29 +21,37 @@ const profile = {
version: packageJson.version
}
enum State {
ok,
cancel,
new
}
export class RemixdHandle extends WebsocketPlugin {
localhostProvider: any
appManager: PluginManager
state: State
constructor (localhostProvider, appManager) {
super(profile)
this.localhostProvider = localhostProvider
this.appManager = appManager
}
deactivate () {
async 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
if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat')
if (this.appManager.actives.includes('slither')) this.appManager.deactivatePlugin('slither')
if (this.appManager.isActive('hardhat')) this.appManager.deactivatePlugin('hardhat')
if (this.appManager.isActive('slither')) this.appManager.deactivatePlugin('slither')
this.localhostProvider.close((error) => {
if (error) console.log(error)
})
}
activate () {
this.connectToLocalhost()
async activate () {
await this.connectToLocalhost()
}
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')
}
@ -65,23 +62,25 @@ export class RemixdHandle extends WebsocketPlugin {
* @param {String} txHash - hash of the transaction
*/
async connectToLocalhost () {
const connection = (error) => {
const connection = (error?:any) => {
if (error) {
console.log(error)
modalDialogCustom.alert(
'Cannot connect to the remixd daemon. ' +
'Please make sure you have the remixd running in the background.'
)
const alert:AlertModal = {
id: 'connectionAlert',
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()
} else {
const intervalId = setInterval(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
clearInterval(intervalId)
console.log(error)
modalDialogCustom.alert(
'Connection to remixd terminated. ' +
'Please make sure remixd is still running in the background.'
)
const alert:AlertModal = {
id: 'connectionAlert',
message: 'Connection to remixd terminated.Please make sure remixd is still running in the background.'
}
this.call('modal', 'alert', alert)
this.canceled()
}
}, 3000)
@ -96,34 +95,38 @@ export class RemixdHandle extends WebsocketPlugin {
this.deactivate()
} else if (!isElectron()) {
// warn the user only if he/she is in the browser context
modalDialog(
'Connect to localhost',
remixdDialog(),
{
label: 'Connect',
fn: () => {
try {
this.localhostProvider.preInit()
super.activate()
setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
connection(new Error('Connection with daemon failed.'))
} else {
connection()
}
}, 3000)
} catch (error) {
connection(error)
}
this.state = State.new
const mod:AppModal = {
id: 'remixdConnect',
title: 'Connect to localhost',
message: remixdDialog(),
okFn: () => {
this.state = State.ok
try {
this.localhostProvider.preInit()
super.activate()
setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
connection(new Error('Connection with daemon failed.'))
} else {
connection()
}
}, 3000)
} catch (error) {
connection(error)
}
},
{
label: 'Cancel',
fn: () => {
this.canceled()
}
cancelFn: async () => {
this.state = State.cancel
await this.canceled()
},
okLabel: 'Connect',
cancelLabel: 'Cancel',
hideFn: async () => {
if (this.state === State.new) await this.canceled()
}
)
}
await this.call('modal', 'modal', mod)
} else {
try {
super.activate()
@ -137,31 +140,31 @@ export class RemixdHandle extends WebsocketPlugin {
function remixdDialog () {
const commandText = 'remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>'
return yo`
<div class=${css.dialog}>
<div class=${css.dialogParagraph}>
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/>
return (<>
<div className=''>
<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/>
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 class=${css.dialogParagraph}>
<div className='mb-2 text-break'>
If you are just looking for the remixd command, here it is:
<br><br><b>${commandText}</b>
<span class="">${copyToClipboard(() => commandText)}</span>
<br></br><br></br><b>${commandText}</b>
<CopyToClipboard data-id='remixdCopyCommand' content={commandText}></CopyToClipboard>
</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>.
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>
</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.
</div>
<div class=${css.dialogParagraph}>
<h6 class="text-danger">
<div className='mb-2 text-break'>
<h6 className="text-danger">
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>
</div>
</div>
`
</>)
}

@ -34,14 +34,14 @@ const profile = {
}
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)
this.event = new EventManager()
this.config = config
this.blockchain = blockchain
this.fileManager = fileManager
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.compilersArtefacts = compilersArtefacts
this.networkModule = networkModule

@ -5,8 +5,6 @@ import * as packageJson from '../../../../../../package.json'
import { ViewPlugin } from '@remixproject/engine-web'
import { RemixUiHomeTab } from '@remix-ui/home-tab' // eslint-disable-line
const GistHandler = require('../../../lib/gist-handler')
const profile = {
name: 'home',
displayName: 'Home',
@ -26,7 +24,6 @@ export class LandingPage extends ViewPlugin {
this.contentImport = contentImport
this.appManager = appManager
this.verticalIcons = verticalIcons
this.gistHandler = new GistHandler()
this.el = document.createElement('div')
this.el.setAttribute('id', 'landingPageHomeContainer')
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 toolTip = require('../app/ui/tooltip')
var GistHandler = require('./gist-handler')
class CmdInterpreterAPI {
constructor (terminal, blockchain) {
@ -17,7 +16,6 @@ class CmdInterpreterAPI {
self._components.registry = Registry.getInstance()
self._components.terminal = terminal
self._components.fileImport = new CompilerImports()
self._components.gistHandler = new GistHandler()
self._deps = {
fileManager: self._components.registry.get('filemanager').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]) }
loadgist (id, cb) {
const self = this
self._components.gistHandler.loadFromGist({ gist: id }, this._deps.fileManager)
this._components.terminal.call('gistHandler', 'load', id)
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 */
import { PluginManager } from '@remixproject/engine'
import { IframePlugin } from '@remixproject/engine-web'
import { EventEmitter } from 'events'
import QueryParams from './lib/query-params'
import { PermissionHandler } from './app/ui/persmission-handler'
import { IframePlugin } from '@remixproject/engine-web'
const _paq = window._paq = window._paq || []
const requiredModules = [ // services + layout views + system views
'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme',
'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)
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)
}
@ -78,6 +78,7 @@ export class RemixAppManager extends PluginManager {
onPluginActivated (plugin) {
this.pluginLoader.set(plugin, this.actives)
this.event.emit('activate', plugin)
this.emit('activate', plugin)
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 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",
"allowJs": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"types": ["node", "jest"],
"module": "es6",
"resolveJsonModule": true
},
"files": [
"../../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 { CompilerArtefacts } from './lib/compiler-artefacts'
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 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])
possiblePaths.push('localhost/node_modules/' + url)
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 { 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 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(() => {
setDragBarPosX(offset + (props.hidden ? 0 : props.refObject.current.offsetWidth))
}, [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) {
setDragState(false)
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 { ModalDialog } from '@remix-ui/modal-dialog'
import { useDialogDispatchers } from '../../context/provider'
const AlertModal = () => {
const [visible, setVisible] = useState<boolean>(true)
const [content, setContent] = useState<string>('')
const OriginWarning = () => {
const { alert } = useDialogDispatchers()
const [content, setContent] = useState<string>(null)
useEffect(() => {
// check the origin and warn message
@ -20,24 +21,15 @@ const AlertModal = () => {
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`)
}
setVisible(content !== '')
}, [])
const closeModal = async () => {
setVisible(false)
}
const handleModalOkClick = async () => {
setVisible(false)
}
return (<ModalDialog
handleHide={closeModal}
id="appAlert"
hide={!visible}
title="Alert"
okLabel="Ok"
okFn={ handleModalOkClick }
cancelLabel=""
cancelFn={closeModal}>{content}</ModalDialog>)
useEffect(() => {
if (content) {
alert({ id: 'warningOriging', title: null, message: content })
}
}, [content])
return (<></>)
}
export default AlertModal
export default OriginWarning

@ -1,5 +1,24 @@
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 RemixSplashScreen from './modals/splashscreen'
import MatomoDialog from './modals/matomo'
import AlertModal from './modals/alert'
import AppContext from './context/context'
import DragBar from './dragbar/dragbar'
import { RemixUIMainPanel } from '@remix-ui/panel'
import RemixSplashScreen from './components/splashscreen'
import MatomoDialog from './components/modals/matomo'
import OriginWarning from './components/modals/origin-warning'
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 {
app: any
}
@ -59,6 +63,13 @@ const RemixApp = (props: IRemixAppUi) => {
props.app.sidePanel.events.on('showing', () => {
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 = {
@ -68,22 +79,32 @@ const RemixApp = (props: IRemixAppUi) => {
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 (
<AppContext.Provider value={{ settings: props.app.settings, showMatamo: props.app.showMatamo, appManager: props.app.appManager }}>
<AppProvider value={value}>
<RemixSplashScreen hide={appReady}></RemixSplashScreen>
<AlertModal></AlertModal>
<OriginWarning></OriginWarning>
<MatomoDialog hide={!appReady}></MatomoDialog>
<div className={`remixIDE ${appReady ? '' : 'd-none'}`} data-id="remixIDE">
{components.iconPanel}
{components.sidePanel}
<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>
{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')
}
const importFromGist = () => {
plugin.gistHandler.loadFromGist({ gist: '' }, fileManager)
plugin.call('gistHandler', 'load', '')
plugin.verticalIcons.select('filePanel')
}
const switchToPreviousVersion = () => {

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

@ -4,7 +4,7 @@ export interface ModalDialogProps {
title?: string,
message?: string | JSX.Element,
okLabel?: string,
okFn?: () => void,
okFn?: (value?:any) => void,
cancelLabel?: string,
cancelFn?: () => void,
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 { PluginBase, Profile } from '@remixproject/plugin-utils'
import { IframePlugin, ViewPlugin, WebsocketPlugin } from '@remixproject/engine-web'
import { IframeReactPlugin } from '@remix-ui/app'
/* eslint-disable camelcase */
interface SetPluginOptionType {
@ -88,7 +89,7 @@ export class PluginManagerComponent extends ViewPlugin extends Plugin implements
render(): HTMLDivElement
getAndFilterPlugins: (filter?: string, profiles?: Profile[]) => void
triggerEngineEventListener: () => void
activateAndRegisterLocalPlugin: (localPlugin: IframePlugin | WebsocketPlugin) => Promise<void>
activateAndRegisterLocalPlugin: (localPlugin: IframePlugin | IframeReactPlugin | WebsocketPlugin) => Promise<void>
activeProfiles: string[]
_paq: any
}

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

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

@ -5,6 +5,7 @@ export class TestTabLogic {
fileManager
currentPath
helper
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor (fileManager: any, helper: any) {
this.fileManager = fileManager
this.helper = helper
@ -26,7 +27,7 @@ export class TestTabLogic {
if (!path || !(/\S/.test(path))) return
path = this.helper.removeMultipleSlashes(path)
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)
})
}
@ -35,11 +36,12 @@ export class TestTabLogic {
// Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
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
}
generateTestFile (errorCb: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
generateTestFile (errorCb:any) {
let fileName = this.fileManager.currentFile()
const hasCurrent = !!fileName && this.fileManager.currentFile().split('.').pop().toLowerCase() === 'sol'
if (!hasCurrent) fileName = this.currentPath + '/newFile.sol'
@ -47,7 +49,7 @@ export class TestTabLogic {
if (!fileProvider) return
const splittedFileName = fileName.split('/')
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)
const isFileCreated = fileProvider.set(newFile, this.generateTestContractSample(hasCurrent, fileName))
if (!isFileCreated) return errorCb('Failed to create test file ' + newFile)
@ -72,7 +74,7 @@ export class TestTabLogic {
let files = []
try {
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
}
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,
// '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))
if (relative === '') relative = '.'
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 type Web3 from 'web3'
import { canUseWorker, urlFromVersion } from '@remix-project/remix-solidity'
import { Renderer } from '@remix-ui/renderer' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import { format } from 'util'
import './css/style.css'
const _paq = (window as any)._paq = (window as any)._paq || [] // eslint-disable-line
/* eslint-disable-next-line */
export interface SolidityUnitTestingProps { }
const _paq = (window as any)._paq = (window as any)._paq || [] // eslint-disable-line @typescript-eslint/no-explicit-any
interface TestObject {
fileName: string
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 { testTabLogic } = testTab
const [toasterMsg, setToasterMsg] = useState('')
const [toasterMsg, setToasterMsg] = useState<string>('')
const [disableCreateButton, setDisableCreateButton] = useState(true)
const [disableGenerateButton, setDisableGenerateButton] = useState(false)
const [disableStopButton, setDisableStopButton] = useState(true)
const [disableRunButton, setDisableRunButton] = useState(false)
const [runButtonTitle, setRunButtonTitle] = useState('Run tests')
const [stopButtonLabel, setStopButtonLabel] = useState('Stop')
const [disableCreateButton, setDisableCreateButton] = useState<boolean>(true)
const [disableGenerateButton, setDisableGenerateButton] = useState<boolean>(false)
const [disableStopButton, setDisableStopButton] = useState<boolean>(true)
const [disableRunButton, setDisableRunButton] = useState<boolean>(false)
const [runButtonTitle, setRunButtonTitle] = useState<string>('Run tests')
const [stopButtonLabel, setStopButtonLabel] = useState<string>('Stop')
const [checkSelectAll, setCheckSelectAll] = useState(true)
const [testsOutput, setTestsOutput] = useState<Element[]>([])
const [checkSelectAll, setCheckSelectAll] = useState<boolean>(true)
const [testsOutput, setTestsOutput] = useState<ReactElement[]>([])
const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState(true)
const [progressBarHidden, setProgressBarHidden] = useState(true)
const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState(true)
const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState<boolean>(true)
const [progressBarHidden, setProgressBarHidden] = useState<boolean>(true)
const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState<boolean>(true)
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 [runningTestsNumber, setRunningTestsNumber] = useState(0) // eslint-disable-line
let [readyTestsNumber, setReadyTestsNumber] = useState<number>(0) // eslint-disable-line
let [runningTestsNumber, setRunningTestsNumber] = useState<number>(0) // eslint-disable-line
const hasBeenStopped = useRef(false)
const isDebugging = useRef(false)
const allTests: any = useRef([])
const selectedTests: any = useRef([])
const currentErrors: any = useRef([])
const hasBeenStopped = useRef<boolean>(false)
const isDebugging = useRef<boolean>(false)
const allTests = useRef<string[]>([])
const selectedTests = useRef<string[]>([])
const currentErrors:any = useRef([]) // eslint-disable-line @typescript-eslint/no-explicit-any
const defaultPath = 'tests'
let areTestsRunning = false
let runningTestFileName: any
const filesContent: any = {}
const testsResultByFilename: Record<string, any> = {}
let runningTestFileName: string
const filesContent: Record<string, Record<string, string>> = {}
const testsResultByFilename: Record<string, Record<string, Record<string, any>>> = {} // eslint-disable-line @typescript-eslint/no-explicit-any
const trimTestDirInput = (input: string) => {
if (input.includes('/')) return input.split('/').map(e => e.trim()).join('/')
@ -71,7 +93,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
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
// Test result, which is compilation error in this case, is not cleared
if (currentErrors.current) {
@ -92,7 +114,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
selectedTests.current = [...allTests.current]
updateTestFileList()
if (!areTestsRunning) await updateRunAction(file)
} catch (e: any) {
} catch (e: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
console.log(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('currentFileChanged', async (file: any, provider: any) => await updateForNewCurrent(file))
testTab.fileManager.events.on('currentFileChanged', async (file: string) => await updateForNewCurrent(file))
}, []) // eslint-disable-line
const updateDirList = (path: string) => {
testTabLogic.dirList(path).then((options: any) => {
testTabLogic.dirList(path).then((options: string[]) => {
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)
testDirInput = helper.removeMultipleSlashes(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
inputPath = helper.removeMultipleSlashes(trimTestDirInput(inputPath))
setInputPathValue(inputPath)
@ -210,18 +232,18 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
setPathOptions(pathOptions)
}
const cleanFileName = (fileName: any, testSuite: any) => {
const cleanFileName = (fileName: string, testSuite: string) => {
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
if (!await testTab.appManager.isActive('debugger')) await testTab.appManager.activatePlugin('debugger')
testTab.call('menuicons', 'select', 'debugger')
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`
for (const log of logsArr) {
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) {
const contractCard: any = (
const contractCard: ReactElement = (
<div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1">
<span className="font-weight-bold">{contract ? contract : ''} ({filename})</span>
</div>
@ -291,20 +313,20 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
</div>)
}
// show contract and file name with label
const ContractCard: any = (
const ContractCard: ReactElement = (
<div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1">
{label}<span className="font-weight-bold">{contract} ({filename})</span>
</div>
)
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
return prevCards
})
}
const renderTests = (tests: any, contract: any, filename: any) => {
const index = tests.findIndex((test: any) => test.type === 'testFailure')
const renderTests = (tests: TestResultInterface[], contract: string, filename: string) => {
const index = tests.findIndex((test: TestResultInterface) => test.type === 'testFailure')
// show filename and contract
renderContract(filename, contract, index)
// show tests
@ -321,7 +343,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
}
if (test.type === 'testPass') {
if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
const testPassCard: any = (
const testPassCard: ReactElement = (
<div
id={runningTestFileName}
data-id="testTabSolidityUnitTestsOutputheader"
@ -339,10 +361,10 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} else if (test.type === 'testFailure') {
if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
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"
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">
<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 method = test.assertMethod === 'ok' ? '' : test.assertMethod
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"
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">
<span> {test.value}</span>
@ -396,7 +418,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
for (const contract of contracts) {
if (contract && contract !== 'summary' && contract !== 'errors') {
runningTestFileName = cleanFileName(filename, contract)
const tests = fileTestsResult[contract]
const tests = fileTestsResult[contract] as TestResultInterface[]
if (tests?.length) {
renderTests(tests, contract, filename)
} else {
@ -406,18 +428,18 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} else if (contract === 'errors' && fileTestsResult['errors']) {
const errors = fileTestsResult['errors']
if (errors && errors.errors) {
errors.errors.forEach((err: any) => {
const errorCard: any = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} />
errors.errors.forEach((err: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const errorCard: ReactElement = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} />
setTestsOutput(prevCards => ([...prevCards, errorCard]))
})
} else if (errors && Array.isArray(errors) && (errors[0].message || errors[0].formattedMessage)) {
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]))
})
} else if (errors && !errors.errors && !Array.isArray(errors)) {
// 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]))
}
}
@ -425,7 +447,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
// show summary
const testSummary = fileTestsResult['summary']
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="text-success">Passed: {testSummary.passed}</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 (!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
// result.passingNum
// result.failureNum
@ -463,7 +485,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
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
setReadyTestsNumber(readyTestsNumber)
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
if (hasBeenStopped.current) {
updateFinalResult(null, null, null)
updateFinalResult(null, null, testFilePath)
return
}
testTab.fileManager.readFile(testFilePath).then((content: any) => {
const runningTests: any = {}
testTab.fileManager.readFile(testFilePath).then((content: string) => {
const runningTests: Record<string, Record<string, string>> = {}
runningTests[testFilePath] = { content }
filesContent[testFilePath] = { content }
const { currentVersion, evmVersion, optimize, runs, isUrl } = testTab.compileTab.getCurrentCompilerConfig()
@ -519,24 +541,24 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
usingWorker: canUseWorker(currentVersion),
runs
}
const deployCb = async (file: any, contractAddress: any) => {
const deployCb = async (file: string, contractAddress: string) => {
const compilerData = await testTab.call('compilerArtefacts', 'getCompilerAbstract', file)
await testTab.call('compilerArtefacts', 'addResolvedContract', contractAddress, compilerData)
}
testTab.testRunner.runTestSources(
runningTests,
compilerConfig,
(result: any) => testCallback(result),
(_err: any, result: any, cb: any) => resultsCallback(_err, result, cb),
(result: Record<string, any>) => testCallback(result), // eslint-disable-line @typescript-eslint/no-explicit-any
(_err: any, result: any, cb: any) => resultsCallback(_err, result, cb), // eslint-disable-line @typescript-eslint/no-explicit-any
deployCb,
(error: any, result: any) => {
(error: any, result: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
updateFinalResult(error, result, testFilePath)
callback(error)
}, (url: any, cb: any) => {
return testTab.contentImport.resolveAndSave(url).then((result: any) => cb(null, result)).catch((error: any) => cb(error.message))
}, (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: Error) => cb(error.message)) // eslint-disable-line @typescript-eslint/no-explicit-any
}, { testFilePath }
)
}).catch((error: any) => {
}).catch((error: Error) => {
console.log(error)
if (error) return // eslint-disable-line
})
@ -552,17 +574,17 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
setDisableStopButton(false)
clearResults()
setProgressBarHidden(false)
const tests = selectedTests.current
const tests: string[] = selectedTests.current
if (!tests || !tests.length) return
else setProgressBarHidden(false)
_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
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')
if (!isSolidityActive || !selectedTests.current?.length) {
// setDisableRunButton(true)
@ -586,7 +608,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
return selectedTestsList.map(testFileObj => testFileObj.fileName)
}
const toggleCheckbox = (eChecked: any, index: any) => {
const toggleCheckbox = (eChecked: boolean, index: number) => {
testFiles[index].checked = eChecked
setTestFiles(testFiles)
selectedTests.current = getCurrentSelectedTests()
@ -603,7 +625,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
} 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)
setTestFiles(testFiles)
setCheckSelectAll(event.target.checked)
@ -618,7 +640,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
const updateTestFileList = () => {
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)
}
else
@ -674,7 +696,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
title="Generate sample test file."
disabled={disableGenerateButton}
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()
}}
>
@ -704,7 +726,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => {
/>
<label className="text-nowrap pl-2 mb-0" htmlFor="checkAllTests"> Select all </label>
</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}`
return (
<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) => {
event.trigger('listenOnNetWork', [isListening])
export const listenOnNetworkAction = async (plugins, isListening) => {
plugins.txListener.setListenOnNetwork(isListening)
}
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 { registerCommandAction, registerLogScriptRunnerAction, registerInfoScriptRunnerAction, registerErrorScriptRunnerAction, registerWarnScriptRunnerAction, listenOnNetworkAction, initListeningOnNetwork } from './actions/terminalAction'
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 { EMPTY_BLOCK, KNOWN_TRANSACTION, RemixUiTerminalProps, UNKNOWN_TRANSACTION } from './types/terminalTypes'
import { wrapScript } from './utils/wrapScript'
import { useDragTerminal } from './custom-hooks/useDragTerminal'
/* eslint-disable-next-line */
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) => {
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 [_cmdTemp, setCmdTemp] = useState('')
const [isOpen, setIsOpen] = useState<boolean>(true)
const [newstate, dispatch] = useReducer(registerCommandReducer, initialState)
const [cmdHistory, cmdHistoryDispatch] = useReducer(addCommandHistoryReducer, initialState)
const [, scriptRunnerDispatch] = useReducer(registerScriptRunnerReducer, initialState)
@ -79,24 +79,6 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
const terminalMenuOffsetHeight = (terminalMenu.current && terminalMenu.current.offsetHeight) || 35
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 = () => {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
}
@ -106,6 +88,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
logHtml: (html) => {
scriptRunnerDispatch({ type: 'html', payload: { message: [html.innerText] } })
},
log: (message) => {
scriptRunnerDispatch({ type: 'log', payload: { message: [message] } })
}
@ -178,7 +161,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
}
function loadgist (id, cb) {
gistHandler.loadFromGist({ gist: id }, _deps.fileManager)
props.plugin.call('gistHandler', 'load', id)
if (cb) cb()
}
@ -332,8 +315,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
const listenOnNetwork = (e: any) => {
const isListening = e.target.checked
// setIsListeningOnNetwork(isListening)
listenOnNetworkAction(event, isListening)
listenOnNetworkAction(props.plugin, isListening)
}
const onChange = (event: any) => {
@ -426,10 +408,14 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: 0, showSuggestions: false }))
}
const handleToggleTerminal = () => {
setIsOpen(!isOpen)
props.plugin.call('layout', 'minimize', props.plugin.profile.name, isOpen)
}
return (
<div style={{ height: '323px', flexGrow: 1 }} className='panel' ref={panelRef}>
<div style={{ flexGrow: 1 }} className='panel' ref={panelRef}>
<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">
<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} >

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

@ -672,9 +672,11 @@ const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: s
return files
}
} 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)
files = _.merge(files, state[state.mode].files[state.mode])
if (deletePath) delete files[deletePath]
return { [state.mode]: files }

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

@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-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",
"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",
@ -162,7 +162,7 @@
"chokidar": "^2.1.8",
"color-support": "^1.1.3",
"commander": "^2.20.3",
"core-js": "^3.19.3",
"core-js": "^3.6.5",
"deep-equal": "^1.0.1",
"document-register-element": "1.13.1",
"ethereumjs-util": "^7.0.10",

@ -69,8 +69,11 @@
"@remix-ui/tabs": ["libs/remix-ui/tabs/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/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/panel": ["libs/remix-ui/panel/src/index.ts"],
"@remix-ui/editor-context-view": ["libs/remix-ui/editor-context-view/src/index.ts"],
"@remix-ui/solidity-unit-testing": [
"libs/remix-ui/solidity-unit-testing/src/index.ts"

@ -82,16 +82,6 @@
"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": {
"root": "libs/remix-ui/solidity-unit-testing",
"sourceRoot": "libs/remix-ui/solidity-unit-testing/src",

Loading…
Cancel
Save