diff --git a/apps/remix-ide-e2e/nightwatch.ts b/apps/remix-ide-e2e/nightwatch.ts index 2bc20e8274..d8a111a617 100644 --- a/apps/remix-ide-e2e/nightwatch.ts +++ b/apps/remix-ide-e2e/nightwatch.ts @@ -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' + ] } } } diff --git a/apps/remix-ide-e2e/src/commands/modalFooterCancelClick.ts b/apps/remix-ide-e2e/src/commands/modalFooterCancelClick.ts index e3f90e0c1b..c6505f8450 100644 --- a/apps/remix-ide-e2e/src/commands/modalFooterCancelClick.ts +++ b/apps/remix-ide-e2e/src/commands/modalFooterCancelClick.ts @@ -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 diff --git a/apps/remix-ide-e2e/src/commands/modalFooterOKClick.ts b/apps/remix-ide-e2e/src/commands/modalFooterOKClick.ts index bea13a01b2..2e656f287c 100644 --- a/apps/remix-ide-e2e/src/commands/modalFooterOKClick.ts +++ b/apps/remix-ide-e2e/src/commands/modalFooterOKClick.ts @@ -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') }) diff --git a/apps/remix-ide-e2e/src/helpers/init.ts b/apps/remix-ide-e2e/src/helpers/init.ts index a5926bd028..e934ae2f7b 100644 --- a/apps/remix-ide-e2e/src/helpers/init.ts +++ b/apps/remix-ide-e2e/src/helpers/init.ts @@ -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, () => { diff --git a/apps/remix-ide-e2e/src/tests/gist.test.ts b/apps/remix-ide-e2e/src/tests/gist.test.ts index 3216ff4b39..8759cd2448 100644 --- a/apps/remix-ide-e2e/src/tests/gist.test.ts +++ b/apps/remix-ide-e2e/src/tests/gist.test.ts @@ -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') diff --git a/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts b/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts index 036d9ceae9..bbf57ca3db 100644 --- a/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts +++ b/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts @@ -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"]') diff --git a/apps/remix-ide-e2e/src/tests/remixd.test.ts b/apps/remix-ide-e2e/src/tests/remixd.test.ts index 38f66665c0..0781733b99 100644 --- a/apps/remix-ide-e2e/src/tests/remixd.test.ts +++ b/apps/remix-ide-e2e/src/tests/remixd.test.ts @@ -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"]') } diff --git a/apps/remix-ide-e2e/src/types/index.d.ts b/apps/remix-ide-e2e/src/types/index.d.ts index 373139fbf4..1ddb10aa10 100644 --- a/apps/remix-ide-e2e/src/types/index.d.ts +++ b/apps/remix-ide-e2e/src/types/index.d.ts @@ -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, diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 0698c90ee5..338af4bbec 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -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) } } diff --git a/apps/remix-ide/src/app/components/hidden-panel.js b/apps/remix-ide/src/app/components/hidden-panel.js deleted file mode 100644 index 77f67c6f4c..0000000000 --- a/apps/remix-ide/src/app/components/hidden-panel.js +++ /dev/null @@ -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` -
- ${this.view} -
` - } -} diff --git a/apps/remix-ide/src/app/components/hidden-panel.tsx b/apps/remix-ide/src/app/components/hidden-panel.tsx new file mode 100644 index 0000000000..bfdff5a11a --- /dev/null +++ b/apps/remix-ide/src/app/components/hidden-panel.tsx @@ -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(} plugins={this.plugins}/>, this.el) + } +} diff --git a/apps/remix-ide/src/app/components/main-panel.js b/apps/remix-ide/src/app/components/main-panel.js deleted file mode 100644 index 66e6a50245..0000000000 --- a/apps/remix-ide/src/app/components/main-panel.js +++ /dev/null @@ -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` -
- ${this.view} -
` - } -} diff --git a/apps/remix-ide/src/app/components/main-panel.tsx b/apps/remix-ide/src/app/components/main-panel.tsx new file mode 100644 index 0000000000..b9d180f194 --- /dev/null +++ b/apps/remix-ide/src/app/components/main-panel.tsx @@ -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(} plugins={this.plugins}/>, this.element) + } +} diff --git a/apps/remix-ide/src/app/components/panel.js b/apps/remix-ide/src/app/components/panel.js deleted file mode 100644 index 08d9abf20f..0000000000 --- a/apps/remix-ide/src/app/components/panel.js +++ /dev/null @@ -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`
` - } - - /** - * 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` -
-
- Loading... -
-
- ` : '' - this.contents[name] = yo`
${view}${loading}
` - - 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) - } -} diff --git a/apps/remix-ide/src/app/components/panel.ts b/apps/remix-ide/src/app/components/panel.ts new file mode 100644 index 0000000000..a5747188af --- /dev/null +++ b/apps/remix-ide/src/app/components/panel.ts @@ -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 = {} + 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) + } +} diff --git a/apps/remix-ide/src/app/components/side-panel.js b/apps/remix-ide/src/app/components/side-panel.js deleted file mode 100644 index 0cc09d84d4..0000000000 --- a/apps/remix-ide/src/app/components/side-panel.js +++ /dev/null @@ -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`
` - 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`` : '' - if (profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)) { - versionWarning = yo`alpha` - } - // Beta - if (profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)) { - versionWarning = yo`beta` - } - } - - const header = yo` -
-
${name}
- ${docLink} - ${versionWarning} -
- ` - yo.update(this.header, header) - } - - render () { - return yo` -
- ${this.header} -
- ${this.view} -
-
` - } -} diff --git a/apps/remix-ide/src/app/components/side-panel.tsx b/apps/remix-ide/src/app/components/side-panel.tsx new file mode 100644 index 0000000000..8b64d64e45 --- /dev/null +++ b/apps/remix-ide/src/app/components/side-panel.tsx @@ -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(} plugins={this.plugins}/>, this.sideelement) + } +} diff --git a/apps/remix-ide/src/app/components/vertical-icons.js b/apps/remix-ide/src/app/components/vertical-icons.js index cd86b1d796..69f6132939 100644 --- a/apps/remix-ide/src/app/components/vertical-icons.js +++ b/apps/remix-ide/src/app/components/vertical-icons.js @@ -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 diff --git a/apps/remix-ide/src/app/files/fileManager.js b/apps/remix-ide/src/app/files/fileManager.ts similarity index 94% rename from apps/remix-ide/src/app/files/fileManager.js rename to apps/remix-ide/src/app/files/fileManager.ts index c526f36600..14aa2d424c 100644 --- a/apps/remix-ide/src/app/files/fileManager.js +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -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 + getFolder: (path: any) => Promise + setFile: (path: any, data: any) => Promise + switchFile: (path: any) => Promise 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) diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 142ff4296e..47e40a2f5f 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -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') diff --git a/apps/remix-ide/src/app/panels/layout.ts b/apps/remix-ide/src/app/panels/layout.ts new file mode 100644 index 0000000000..451c2aefe7 --- /dev/null +++ b/apps/remix-ide/src/app/panels/layout.ts @@ -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 { + 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) + } +} diff --git a/apps/remix-ide/src/app/panels/main-view.js b/apps/remix-ide/src/app/panels/main-view.js deleted file mode 100644 index bf3a07a35b..0000000000 --- a/apps/remix-ide/src/app/panels/main-view.js +++ /dev/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` -
- ${self.tabProxy.renderTabsbar()} - ${self._view.editor} - ${self._view.mainPanel} -
- ${self._view.terminal} -
- ` - - // 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) - } -} diff --git a/apps/remix-ide/src/app/panels/tab-proxy.js b/apps/remix-ide/src/app/panels/tab-proxy.js index 5b65254830..267b54d853 100644 --- a/apps/remix-ide/src/app/panels/tab-proxy.js +++ b/apps/remix-ide/src/app/panels/tab-proxy.js @@ -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 } } diff --git a/apps/remix-ide/src/app/panels/terminal.js b/apps/remix-ide/src/app/panels/terminal.js index 4837f24203..6309327cb6 100644 --- a/apps/remix-ide/src/app/panels/terminal.js +++ b/apps/remix-ide/src/app/panels/terminal.js @@ -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') diff --git a/apps/remix-ide/src/app/plugins/modal.tsx b/apps/remix-ide/src/app/plugins/modal.tsx new file mode 100644 index 0000000000..f69cfda0d4 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/modal.tsx @@ -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 = { + name: 'modal', + displayName: 'Modal', + description: 'Modal', + methods: ['modal', 'alert', 'toast'] +} + +export class ModalPlugin extends Plugin implements MethodApi { + 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) + } +} diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/plugins/remixd-handle.tsx similarity index 56% rename from apps/remix-ide/src/app/files/remixd-handle.js rename to apps/remix-ide/src/app/plugins/remixd-handle.tsx index be6997574d..3b6d27c815 100644 --- a/apps/remix-ide/src/app/files/remixd-handle.js +++ b/apps/remix-ide/src/app/plugins/remixd-handle.tsx @@ -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 -u ' - return yo` -
-
- Access your local file system from Remix IDE using Remixd NPM package.

+ return (<> +
+
+ Access your local file system from Remix IDE using Remixd NPM package.

Remixd needs to be running in the background to load the files in localhost workspace. For more info, please check the Remixd tutorial.
-
+
If you are just looking for the remixd command, here it is: -

${commandText} - ${copyToClipboard(() => commandText)} +



${commandText} +
-
+
When connected, a session will be started between ${window.location.origin} and your local file system at ws://127.0.0.1:65520. - The shared folder will be in the "File Explorers" workspace named "localhost". + The shared folder will be in the "File Explorers" workspace named "localhost".
Read more about other Remixd ports usage
-
+
This feature is still in Alpha. We recommend to keep a backup of the shared folder.
-
-
+
+
Before using, make sure remixd version is latest i.e. ${remixdVersion} -
Read here how to update it +

Read here how to update it
- ` + ) } diff --git a/apps/remix-ide/src/app/udapp/run-tab.js b/apps/remix-ide/src/app/udapp/run-tab.js index b966287690..48f5484979 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.js +++ b/apps/remix-ide/src/app/udapp/run-tab.js @@ -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`
${msg}
`) } + this.logCallback = (msg) => { this.call('terminal', 'logHtml', yo`
${msg}
`) } this.filePanel = filePanel this.compilersArtefacts = compilersArtefacts this.networkModule = networkModule diff --git a/apps/remix-ide/src/app/ui/landing-page/landing-page.js b/apps/remix-ide/src/app/ui/landing-page/landing-page.js index a100c4ea4e..b05271dc96 100644 --- a/apps/remix-ide/src/app/ui/landing-page/landing-page.js +++ b/apps/remix-ide/src/app/ui/landing-page/landing-page.js @@ -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') diff --git a/apps/remix-ide/src/framingService.js b/apps/remix-ide/src/framingService.js deleted file mode 100644 index ed029b7a65..0000000000 --- a/apps/remix-ide/src/framingService.js +++ /dev/null @@ -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() - } -} diff --git a/apps/remix-ide/src/lib/cmdInterpreterAPI.js b/apps/remix-ide/src/lib/cmdInterpreterAPI.js index 530846873d..82ae4636fe 100644 --- a/apps/remix-ide/src/lib/cmdInterpreterAPI.js +++ b/apps/remix-ide/src/lib/cmdInterpreterAPI.js @@ -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() } diff --git a/apps/remix-ide/src/lib/gist-handler.js b/apps/remix-ide/src/lib/gist-handler.js deleted file mode 100644 index da881e1103..0000000000 --- a/apps/remix-ide/src/lib/gist-handler.js +++ /dev/null @@ -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 diff --git a/apps/remix-ide/src/lib/panels-resize.js b/apps/remix-ide/src/lib/panels-resize.js deleted file mode 100644 index 157535a869..0000000000 --- a/apps/remix-ide/src/lib/panels-resize.js +++ /dev/null @@ -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`
` - - 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`
` - } - - 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' - } -} diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 32afcaec9a..5f5fa2f8eb 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -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) }) } diff --git a/apps/remix-ide/test/compiler-test.js b/apps/remix-ide/test/compiler-test.js deleted file mode 100644 index 5af70fd14f..0000000000 --- a/apps/remix-ide/test/compiler-test.js +++ /dev/null @@ -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) -}) diff --git a/apps/remix-ide/test/gist-handler-test.js b/apps/remix-ide/test/gist-handler-test.js deleted file mode 100644 index 07eaad830c..0000000000 --- a/apps/remix-ide/test/gist-handler-test.js +++ /dev/null @@ -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 diff --git a/apps/remix-ide/test/index.js b/apps/remix-ide/test/index.js deleted file mode 100644 index 84d8a5c68f..0000000000 --- a/apps/remix-ide/test/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -require('./compiler-test') -require('./gist-handler-test') -require('./query-params-test') diff --git a/apps/remix-ide/test/query-params-test.js b/apps/remix-ide/test/query-params-test.js deleted file mode 100644 index c9062f6da0..0000000000 --- a/apps/remix-ide/test/query-params-test.js +++ /dev/null @@ -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') -}) diff --git a/apps/remix-ide/tsconfig.json b/apps/remix-ide/tsconfig.json index b3e2e7d0ca..04d219a926 100644 --- a/apps/remix-ide/tsconfig.json +++ b/apps/remix-ide/tsconfig.json @@ -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", diff --git a/libs/remix-core-plugin/src/index.ts b/libs/remix-core-plugin/src/index.ts index fe8a5c661e..b91e6b76a6 100644 --- a/libs/remix-core-plugin/src/index.ts +++ b/libs/remix-core-plugin/src/index.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' diff --git a/libs/remix-core-plugin/src/lib/compiler-content-imports.ts b/libs/remix-core-plugin/src/lib/compiler-content-imports.ts index 8c243e37a8..7eab9205ce 100644 --- a/libs/remix-core-plugin/src/lib/compiler-content-imports.ts +++ b/libs/remix-core-plugin/src/lib/compiler-content-imports.ts @@ -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]) diff --git a/libs/remix-core-plugin/src/lib/gist-handler.ts b/libs/remix-core-plugin/src/lib/gist-handler.ts new file mode 100644 index 0000000000..6c22c29aa8 --- /dev/null +++ b/libs/remix-core-plugin/src/lib/gist-handler.ts @@ -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 +} diff --git a/libs/remix-ui/app/src/index.ts b/libs/remix-ui/app/src/index.ts index e47f8690e7..d02ade520c 100644 --- a/libs/remix-ui/app/src/index.ts +++ b/libs/remix-ui/app/src/index.ts @@ -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' diff --git a/libs/remix-ui/app/src/lib/remix-app/actions/modals.ts b/libs/remix-ui/app/src/lib/remix-app/actions/modals.ts new file mode 100644 index 0000000000..95400ca77b --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/actions/modals.ts @@ -0,0 +1,30 @@ +import { AppModal } from '../interface' + +type ActionMap = { + [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[keyof ActionMap< + ModalPayload +>] diff --git a/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.css b/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.css new file mode 100644 index 0000000000..1330c1179c --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.css @@ -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; +} diff --git a/libs/remix-ui/app/src/lib/remix-app/dragbar/dragbar.tsx b/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx similarity index 77% rename from libs/remix-ui/app/src/lib/remix-app/dragbar/dragbar.tsx rename to libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx index 29c4eeda6b..429e49f638 100644 --- a/libs/remix-ui/app/src/lib/remix-app/dragbar/dragbar.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx @@ -15,18 +15,23 @@ const DragBar = (props: IRemixDragBarUi) => { const [offset, setOffSet] = useState(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) { diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/dialogViewPlugin.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/dialogViewPlugin.tsx new file mode 100644 index 0000000000..d5610de56c --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/dialogViewPlugin.tsx @@ -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 diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/dialogs.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/dialogs.tsx new file mode 100644 index 0000000000..7aba74f73b --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/dialogs.tsx @@ -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 ( + <> + + + ) +} +export default AppDialogs diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx new file mode 100644 index 0000000000..4d4daaed3e --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx @@ -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(props.hide) + + const message = () => { + return (<>

An Opt-in version of Matomo, an open source data analytics platform is being used to improve Remix IDE.

+

We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.

+

All data collected through Matomo is stored on our own server - no data is ever given to third parties. Our analytics reports are public: take a look.

+

We do not collect nor store any personally identifiable information (PII).

+

For more info, see: Matomo Analyitcs on Remix iDE.

+

You can change your choice in the Settings panel anytime.

) + } + + 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 diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx new file mode 100644 index 0000000000..cff3215e1f --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx @@ -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() + 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} + + ) + } + + 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 ( + + ) +} +export default ModalWrapper diff --git a/libs/remix-ui/app/src/lib/remix-app/modals/alert.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/origin-warning.tsx similarity index 68% rename from libs/remix-ui/app/src/lib/remix-app/modals/alert.tsx rename to libs/remix-ui/app/src/lib/remix-app/components/modals/origin-warning.tsx index b61abbb1d1..61d25db2f6 100644 --- a/libs/remix-ui/app/src/lib/remix-app/modals/alert.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/origin-warning.tsx @@ -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(true) - const [content, setContent] = useState('') +const OriginWarning = () => { + const { alert } = useDialogDispatchers() + const [content, setContent] = useState(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 ({content}) + useEffect(() => { + if (content) { + alert({ id: 'warningOriging', title: null, message: content }) + } + }, [content]) + + return (<>) } -export default AlertModal +export default OriginWarning diff --git a/libs/remix-ui/app/src/lib/remix-app/modals/splashscreen.tsx b/libs/remix-ui/app/src/lib/remix-app/components/splashscreen.tsx similarity index 100% rename from libs/remix-ui/app/src/lib/remix-app/modals/splashscreen.tsx rename to libs/remix-ui/app/src/lib/remix-app/components/splashscreen.tsx diff --git a/libs/remix-ui/app/src/lib/remix-app/context/context.tsx b/libs/remix-ui/app/src/lib/remix-app/context/context.tsx index f9f97a4555..3745fcddf0 100644 --- a/libs/remix-ui/app/src/lib/remix-app/context/context.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/context/context.tsx @@ -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(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({ + modal: (data: AppModal) => { }, + toast: (message: string) => {}, + alert: (data: AlertModal) => {}, + handleHideModal: () => {}, + handleToaster: () => {} +}) + +export const modalContext = React.createContext(ModalInitialState) diff --git a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx new file mode 100644 index 0000000000..73a3a97305 --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx @@ -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 ( + + {children} + + ) +} + +export const AppProvider = ({ children = [], value = {} } = {}) => { + return + {children} + +} + +export const useDialogs = () => { + return React.useContext(modalContext) +} + +export const useDialogDispatchers = () => { + return React.useContext(dispatchModalContext) +} diff --git a/libs/remix-ui/app/src/lib/remix-app/dragbar/dragbar.css b/libs/remix-ui/app/src/lib/remix-app/dragbar/dragbar.css deleted file mode 100644 index a423dd6605..0000000000 --- a/libs/remix-ui/app/src/lib/remix-app/dragbar/dragbar.css +++ /dev/null @@ -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; - } \ No newline at end of file diff --git a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts new file mode 100644 index 0000000000..dcfcd7a1eb --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts @@ -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 +} diff --git a/libs/remix-ui/app/src/lib/remix-app/modals/matomo.tsx b/libs/remix-ui/app/src/lib/remix-app/modals/matomo.tsx deleted file mode 100644 index 2ab56fa02d..0000000000 --- a/libs/remix-ui/app/src/lib/remix-app/modals/matomo.tsx +++ /dev/null @@ -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(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 ( -

An Opt-in version of Matomo, an open source data analytics platform is being used to improve Remix IDE.

-

We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.

-

All data collected through Matomo is stored on our own server - no data is ever given to third parties. Our analytics reports are public: take a look.

-

We do not collect nor store any personally identifiable information (PII).

-

For more info, see: Matomo Analyitcs on Remix iDE.

-

You can change your choice in the Settings panel anytime.

-
) -} - -export default MatomoDialog diff --git a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts new file mode 100644 index 0000000000..cf202585f4 --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts @@ -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: '' } + } +} diff --git a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx index d8430eac52..27d5a7b832 100644 --- a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx @@ -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:
} + const value = { + settings: props.app.settings, + showMatamo: props.app.showMatamo, + appManager: props.app.appManager, + modal: props.app.modal, + layout: props.app.layout + } + return ( - + - +
{components.iconPanel} {components.sidePanel} - {components.mainPanel} - +
+ +
{components.hiddenPanel} -
- + + + ) } diff --git a/libs/remix-ui/app/src/lib/remix-app/state/modals.ts b/libs/remix-ui/app/src/lib/remix-app/state/modals.ts new file mode 100644 index 0000000000..8332d60120 --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/state/modals.ts @@ -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: '' +} diff --git a/libs/remix-ui/app/src/lib/remix-app/types/index.ts b/libs/remix-ui/app/src/lib/remix-app/types/index.ts new file mode 100644 index 0000000000..edca9e147c --- /dev/null +++ b/libs/remix-ui/app/src/lib/remix-app/types/index.ts @@ -0,0 +1,7 @@ +export const enum ModalTypes { + alert = 'alert', + confirm = 'confirm', + prompt = 'prompt', + password = 'password', + default = 'default', +} diff --git a/libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx b/libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx index 4c41c7968f..c130d35a64 100644 --- a/libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx +++ b/libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx @@ -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 = () => { diff --git a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx index bd3a07cf00..714255aa93 100644 --- a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx +++ b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx @@ -12,12 +12,15 @@ export const ModalDialog = (props: ModalDialogProps) => { const [state, setState] = useState({ toggleBtn: true }) + const calledHideFunctionOnce = useRef() 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} {!props.showCancelIcon && - handleHide()}> - - + handleHide()}> + + }
- { props.children ? props.children : props.message } + {props.children ? props.children : props.message}
{/* todo add autofocus ^^ */} - { props.okLabel && - { - if (props.okFn) props.okFn() - handleHide() - }} - > - { props.okLabel ? props.okLabel : 'OK' } - + { props.okLabel && { + if (props.okFn) props.okFn() + handleHide() + }} + > + {props.okLabel ? props.okLabel : 'OK'} + } - { props.cancelLabel && - { - if (props.cancelFn) props.cancelFn() - handleHide() - }} - > - { props.cancelLabel ? props.cancelLabel : 'Cancel' } - + { props.cancelLabel && { + if (props.cancelFn) props.cancelFn() + handleHide() + }} + > + {props.cancelLabel ? props.cancelLabel : 'Cancel'} + }
diff --git a/libs/remix-ui/modal-dialog/src/lib/types/index.ts b/libs/remix-ui/modal-dialog/src/lib/types/index.ts index b03cb16c94..acb171fc0f 100644 --- a/libs/remix-ui/modal-dialog/src/lib/types/index.ts +++ b/libs/remix-ui/modal-dialog/src/lib/types/index.ts @@ -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, diff --git a/libs/remix-ui/panel/.babelrc b/libs/remix-ui/panel/.babelrc new file mode 100644 index 0000000000..64a3748691 --- /dev/null +++ b/libs/remix-ui/panel/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} \ No newline at end of file diff --git a/libs/remix-ui/panel/.eslintrc.json b/libs/remix-ui/panel/.eslintrc.json new file mode 100644 index 0000000000..5a1c541d11 --- /dev/null +++ b/libs/remix-ui/panel/.eslintrc.json @@ -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": {} + } + ] +} \ No newline at end of file diff --git a/libs/remix-ui/panel/README.md b/libs/remix-ui/panel/README.md new file mode 100644 index 0000000000..fa765fcb39 --- /dev/null +++ b/libs/remix-ui/panel/README.md @@ -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). diff --git a/libs/remix-ui/panel/src/index.ts b/libs/remix-ui/panel/src/index.ts new file mode 100644 index 0000000000..c61d9612f1 --- /dev/null +++ b/libs/remix-ui/panel/src/index.ts @@ -0,0 +1,2 @@ +export { default as RemixPluginPanel } from './lib/plugins/remix-ui-panel' +export { default as RemixUIMainPanel } from './lib/main/main-panel' diff --git a/libs/remix-ui/panel/src/lib/dragbar/dragbar.css b/libs/remix-ui/panel/src/lib/dragbar/dragbar.css new file mode 100644 index 0000000000..1ad9f9de85 --- /dev/null +++ b/libs/remix-ui/panel/src/lib/dragbar/dragbar.css @@ -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; +} diff --git a/libs/remix-ui/panel/src/lib/dragbar/dragbar.tsx b/libs/remix-ui/panel/src/lib/dragbar/dragbar.tsx new file mode 100644 index 0000000000..232f23ff10 --- /dev/null +++ b/libs/remix-ui/panel/src/lib/dragbar/dragbar.tsx @@ -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; + setHideStatus: (hide: boolean) => void; + hidden: boolean + minHeight?: number +} + +const DragBar = (props: IRemixDragBarUi) => { + const [dragState, setDragState] = useState(false) + const [dragBarPosY, setDragBarPosY] = useState(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 <> +
+ +
+
+ +} + +export default DragBar diff --git a/libs/remix-ui/panel/src/lib/main/main-panel.css b/libs/remix-ui/panel/src/lib/main/main-panel.css new file mode 100644 index 0000000000..d569338fab --- /dev/null +++ b/libs/remix-ui/panel/src/lib/main/main-panel.css @@ -0,0 +1,8 @@ +.mainview { + display : flex; + flex-direction : column; + height : 100%; + width : 100%; + position: relative; + } + diff --git a/libs/remix-ui/panel/src/lib/main/main-panel.tsx b/libs/remix-ui/panel/src/lib/main/main-panel.tsx new file mode 100644 index 0000000000..4fb00ddf65 --- /dev/null +++ b/libs/remix-ui/panel/src/lib/main/main-panel.tsx @@ -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([]) + const editorRef = useRef(null) + const mainPanelRef = useRef(null) + const tabsRef = useRef(null) + const terminalRef = useRef(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 ( +
+ {Object.values(plugins).map((pluginRecord, i) => { + return ( + + {(pluginRecord.profile.name === 'terminal') ? : null} + + + ) + })} +
+ ) +} + +export default RemixUIMainPanel diff --git a/libs/remix-ui/panel/src/lib/plugins/panel-header.tsx b/libs/remix-ui/panel/src/lib/plugins/panel-header.tsx new file mode 100644 index 0000000000..5acd50541a --- /dev/null +++ b/libs/remix-ui/panel/src/lib/plugins/panel-header.tsx @@ -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; + } +const RemixUIPanelHeader = (props: RemixPanelProps) => { + const [plugin, setPlugin] = useState() + + useEffect(() => { + if (props.plugins) { + const p = Object.values(props.plugins).find((pluginRecord) => { + return pluginRecord.active === true + }) + setPlugin(p) + } + }, [props]) + + return ( +
{plugin?.profile.displayName || plugin?.profile.name}
+ {plugin?.profile.documentation ? () : ''} +
) +} + +export default RemixUIPanelHeader diff --git a/libs/remix-ui/panel/src/lib/plugins/panel-plugin.tsx b/libs/remix-ui/panel/src/lib/plugins/panel-plugin.tsx new file mode 100644 index 0000000000..9eb30391bc --- /dev/null +++ b/libs/remix-ui/panel/src/lib/plugins/panel-plugin.tsx @@ -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(null) + const [view, setView] = useState() + 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 ( +
+ {view} +
+ ) +} + +export default forwardRef(RemixUIPanelPlugin) diff --git a/libs/remix-ui/panel/src/lib/plugins/panel.css b/libs/remix-ui/panel/src/lib/plugins/panel.css new file mode 100644 index 0000000000..d2b2133667 --- /dev/null +++ b/libs/remix-ui/panel/src/lib/plugins/panel.css @@ -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; +} diff --git a/libs/remix-ui/panel/src/lib/plugins/remix-ui-panel.tsx b/libs/remix-ui/panel/src/lib/plugins/remix-ui-panel.tsx new file mode 100644 index 0000000000..37fa018c92 --- /dev/null +++ b/libs/remix-ui/panel/src/lib/plugins/remix-ui-panel.tsx @@ -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 + header: JSX.Element +} + +export function RemixPluginPanel (props: RemixPanelProps) { + return ( + <> + {props.header} +
+
+ {Object.values(props.plugins).map((pluginRecord) => { + return + })} +
+
+ + + ) +} + +export default RemixPluginPanel diff --git a/libs/remix-ui/panel/src/lib/types/index.ts b/libs/remix-ui/panel/src/lib/types/index.ts new file mode 100644 index 0000000000..f8407033ab --- /dev/null +++ b/libs/remix-ui/panel/src/lib/types/index.ts @@ -0,0 +1,9 @@ +import { Profile } from '@remixproject/plugin-utils' + +export type PluginRecord = { + profile: Profile + view: any + active: boolean + class?: string + minimized?: boolean + } diff --git a/libs/remix-ui/panel/tsconfig.json b/libs/remix-ui/panel/tsconfig.json new file mode 100644 index 0000000000..8bd701c578 --- /dev/null +++ b/libs/remix-ui/panel/tsconfig.json @@ -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" + } + ] +} diff --git a/libs/remix-ui/panel/tsconfig.lib.json b/libs/remix-ui/panel/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/panel/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/remix-ui/plugin-manager/src/types.d.ts b/libs/remix-ui/plugin-manager/src/types.d.ts index d30ae31457..66ee57bc77 100644 --- a/libs/remix-ui/plugin-manager/src/types.d.ts +++ b/libs/remix-ui/plugin-manager/src/types.d.ts @@ -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 + activateAndRegisterLocalPlugin: (localPlugin: IframePlugin | IframeReactPlugin | WebsocketPlugin) => Promise activeProfiles: string[] _paq: any } diff --git a/libs/remix-ui/solidity-unit-testing/.babelrc b/libs/remix-ui/solidity-unit-testing/.babelrc index 09d67939cc..64a3748691 100644 --- a/libs/remix-ui/solidity-unit-testing/.babelrc +++ b/libs/remix-ui/solidity-unit-testing/.babelrc @@ -1,4 +1,4 @@ { "presets": ["@nrwl/react/babel"], "plugins": [] -} +} \ No newline at end of file diff --git a/libs/remix-ui/solidity-unit-testing/.eslintrc.json b/libs/remix-ui/solidity-unit-testing/.eslintrc.json index 5e100bf955..50e59482cf 100644 --- a/libs/remix-ui/solidity-unit-testing/.eslintrc.json +++ b/libs/remix-ui/solidity-unit-testing/.eslintrc.json @@ -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"], diff --git a/libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts b/libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts index 6b42bc128a..6e1af409a4 100644 --- a/libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts +++ b/libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts @@ -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 + '_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)}";` : '// ' diff --git a/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx index 0e791fd0ec..6cb5bc3820 100644 --- a/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx +++ b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx @@ -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) => { +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) => { // eslint-disable-line @typescript-eslint/no-explicit-any const { helper, testTab, initialPath } = props const { testTabLogic } = testTab - const [toasterMsg, setToasterMsg] = useState('') + const [toasterMsg, setToasterMsg] = useState('') - 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(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 [checkSelectAll, setCheckSelectAll] = useState(true) - const [testsOutput, setTestsOutput] = useState([]) + const [checkSelectAll, setCheckSelectAll] = useState(true) + const [testsOutput, setTestsOutput] = useState([]) - const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState(true) - const [progressBarHidden, setProgressBarHidden] = useState(true) - const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState(true) + const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState(true) + const [progressBarHidden, setProgressBarHidden] = useState(true) + const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState(true) let [testFiles, setTestFiles] = useState([]) // eslint-disable-line - const [pathOptions, setPathOptions] = useState(['']) + const [pathOptions, setPathOptions] = useState(['']) - const [inputPathValue, setInputPathValue] = useState('tests') + const [inputPathValue, setInputPathValue] = useState('tests') - let [readyTestsNumber, setReadyTestsNumber] = useState(0) // eslint-disable-line - let [runningTestsNumber, setRunningTestsNumber] = useState(0) // eslint-disable-line + let [readyTestsNumber, setReadyTestsNumber] = useState(0) // eslint-disable-line + let [runningTestsNumber, setRunningTestsNumber] = useState(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(false) + const isDebugging = useRef(false) + const allTests = useRef([]) + const selectedTests = useRef([]) + 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 = {} + let runningTestFileName: string + const filesContent: Record> = {} + const testsResultByFilename: Record>> = {} // 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) => { 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) => { 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) => { }) 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) => { } } - 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) => { 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[], testName: string) => { // eslint-disable-line @typescript-eslint/no-explicit-any let finalLogs = `${testName}:\n` for (const log of logsArr) { let formattedLog @@ -262,9 +284,9 @@ export const SolidityUnitTesting = (props: Record) => { } } - 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 = (
{contract ? contract : ''} ({filename})
@@ -291,20 +313,20 @@ export const SolidityUnitTesting = (props: Record) => {
) } // show contract and file name with label - const ContractCard: any = ( + const ContractCard: ReactElement = (
{label}{contract} ({filename})
) 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) => { } if (test.type === 'testPass') { if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value) - const testPassCard: any = ( + const testPassCard: ReactElement = (
) => { } else if (test.type === 'testFailure') { if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value) if (!test.assertMethod) { - const testFailCard1: any = (
highlightLocation(test.location, test.filename)} + onClick={() => { if(test.location) highlightLocation(test.location, test.filename)}} >
✘ {test.value} @@ -356,10 +378,10 @@ export const SolidityUnitTesting = (props: Record) => { 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 = (
highlightLocation(test.location, test.filename)} + onClick={() => { if(test.location) highlightLocation(test.location, test.filename)}} >
✘ {test.value} @@ -396,7 +418,7 @@ export const SolidityUnitTesting = (props: Record) => { 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) => { } else if (contract === 'errors' && fileTestsResult['errors']) { const errors = fileTestsResult['errors'] if (errors && errors.errors) { - errors.errors.forEach((err: any) => { - const errorCard: any = + errors.errors.forEach((err: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any + const errorCard: ReactElement = setTestsOutput(prevCards => ([...prevCards, errorCard])) }) } else if (errors && Array.isArray(errors) && (errors[0].message || errors[0].formattedMessage)) { errors.forEach((err) => { - const errorCard: any = + const errorCard: ReactElement = 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 = + const errorCard: ReactElement = setTestsOutput(prevCards => ([...prevCards, errorCard])) } } @@ -425,7 +447,7 @@ export const SolidityUnitTesting = (props: Record) => { // show summary const testSummary = fileTestsResult['summary'] if (testSummary && testSummary.filename && !testSummary.rendered) { - const summaryCard: any = (
+ const summaryCard: ReactElement = (
Result for {testSummary.filename} Passed: {testSummary.passed} Failed: {testSummary.failed} @@ -437,7 +459,7 @@ export const SolidityUnitTesting = (props: Record) => { } } - const testCallback = (result: any) => { + const testCallback = (result: Record) => { // 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) => { } } - 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) => { 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) => { } } - 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> = {} runningTests[testFilePath] = { content } filesContent[testFilePath] = { content } const { currentVersion, evmVersion, optimize, runs, isUrl } = testTab.compileTab.getCurrentCompilerConfig() @@ -519,24 +541,24 @@ export const SolidityUnitTesting = (props: Record) => { 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) => 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) => { 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) => { 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) => { } 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) => { 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) => { 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) => { />
-
{testFiles?.length ? testFiles.map((testFileObj: any, index) => { +
{testFiles?.length ? testFiles.map((testFileObj: TestObject, index) => { const elemId = `singleTest${testFileObj.fileName}` return (
diff --git a/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts b/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts index dafe6f78cf..d0efd289b5 100644 --- a/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts +++ b/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts @@ -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) => { diff --git a/libs/remix-ui/terminal/src/lib/custom-hooks/useDragTerminal.tsx b/libs/remix-ui/terminal/src/lib/custom-hooks/useDragTerminal.tsx deleted file mode 100644 index aba4bbf6a5..0000000000 --- a/libs/remix-ui/terminal/src/lib/custom-hooks/useDragTerminal.tsx +++ /dev/null @@ -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) => { - 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 - } -} diff --git a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx index acb13ecd68..3536750efc 100644 --- a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx @@ -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 extends SyntheticEvent { @@ -25,10 +25,10 @@ export interface ClipboardEvent extends SyntheticEvent { } 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(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 ( -
+
-
diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index fddabeba59..3dd6b16bbb 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -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)) } }) diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index e046892df9..ef769f5c5b 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -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 } diff --git a/nx.json b/nx.json index e1bc0d858a..f2aa009899 100644 --- a/nx.json +++ b/nx.json @@ -145,6 +145,9 @@ "remix-ui-tabs": { "tags": [] }, + "remix-ui-panel": { + "tags": [] + }, "remix-ui-theme-module": { "tags": [] }, diff --git a/package.json b/package.json index a32a38bdb3..0a838a7a40 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tsconfig.base.json b/tsconfig.base.json index 4ef2c10dcb..21dfeb4ee8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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" diff --git a/workspace.json b/workspace.json index 1718e70f69..76b236717f 100644 --- a/workspace.json +++ b/workspace.json @@ -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",