diff --git a/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts b/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts index fe0b45af576..83f8d426cad 100644 --- a/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts +++ b/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts @@ -4,7 +4,7 @@ import EventEmitter from 'events' class CurrentWorkspaceIs extends EventEmitter { command (this: NightwatchBrowser, name: string): NightwatchBrowser { const browser = this.api - const xpath = `//*[@data-id="workspacesSelect"]//*[@data-id="dropdown-content"][contains(normalize-space(), "${name}")]`; + const xpath = `//*[@data-id="workspacesSelect"]//*[@data-id="workspacesSelect-togglerText"][contains(normalize-space(), "${name}")]`; browser.waitForElementVisible({ locateStrategy: 'xpath', diff --git a/apps/remix-ide-e2e/src/commands/scrollInto.ts b/apps/remix-ide-e2e/src/commands/scrollInto.ts index f477eac15fb..1c8fd16a416 100644 --- a/apps/remix-ide-e2e/src/commands/scrollInto.ts +++ b/apps/remix-ide-e2e/src/commands/scrollInto.ts @@ -14,7 +14,7 @@ class ScrollInto extends EventEmitter { } function _scrollInto (browser: NightwatchBrowser, target: string, cb: VoidFunction): void { - browser.execute(function (target) { + browser.executeScript(function (target) { document.querySelector(target).scrollIntoView(({ block: 'center' })) }, [target], function () { cb() diff --git a/apps/remix-ide-e2e/src/helpers/init.ts b/apps/remix-ide-e2e/src/helpers/init.ts index 6f1ba8ec92f..80dcacd24b9 100644 --- a/apps/remix-ide-e2e/src/helpers/init.ts +++ b/apps/remix-ide-e2e/src/helpers/init.ts @@ -28,7 +28,7 @@ export default function (browser: NightwatchBrowser, callback: VoidFunction, url .verifyLoad() .enableClipBoard() .perform((done) => { - browser.execute(function () { // hide tooltips + browser.execute(function () { // hide tooltips for Bootstrap 5 function addStyle(styleString) { const style = document.createElement('style'); style.textContent = styleString; @@ -36,13 +36,38 @@ export default function (browser: NightwatchBrowser, callback: VoidFunction, url } addStyle(` - .popover { - display:none !important; + .popover, + .tooltip, + .bs-popover-auto, + .bs-tooltip-auto, + .bs-popover-top, + .bs-popover-bottom, + .bs-popover-start, + .bs-popover-end, + .bs-tooltip-top, + .bs-tooltip-bottom, + .bs-tooltip-start, + .bs-tooltip-end { + display: none !important; + opacity: 0 !important; + visibility: hidden !important; } #scamDetails { - display:none !important; + display: none !important; } - `); + `); + + // Additionally, programmatically disable all Bootstrap 5 tooltips + if ((window as any).bootstrap && typeof (window as any).bootstrap.Tooltip === 'function') { + const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.forEach(function (tooltipTriggerEl) { + try { + const tooltipInstance = (window as any).bootstrap.Tooltip.getInstance(tooltipTriggerEl) || new (window as any).bootstrap.Tooltip(tooltipTriggerEl); + tooltipInstance.disable && tooltipInstance.disable(); + tooltipInstance.hide && tooltipInstance.hide(); + } catch (e) {} + }); + } }, [], done()) }) .perform(() => { diff --git a/apps/remix-ide-e2e/src/local-plugin/src/app/app.tsx b/apps/remix-ide-e2e/src/local-plugin/src/app/app.tsx index e72d5c8ac1a..08cfba2d720 100644 --- a/apps/remix-ide-e2e/src/local-plugin/src/app/app.tsx +++ b/apps/remix-ide-e2e/src/local-plugin/src/app/app.tsx @@ -1,24 +1,28 @@ -import React, {useEffect, useState} from 'react' -import {RemixPlugin} from './Client' -import {Logger} from './logger' -import {filePanelProfile} from '@remixproject/plugin-api' -import {filSystemProfile} from '@remixproject/plugin-api' -import {editorProfile} from '@remixproject/plugin-api' -import {settingsProfile} from '@remixproject/plugin-api' -import {networkProfile} from '@remixproject/plugin-api' -import {udappProfile} from '@remixproject/plugin-api' -import {compilerProfile} from '@remixproject/plugin-api' -import {contentImportProfile} from '@remixproject/plugin-api' -import {windowProfile} from '@remixproject/plugin-api' -import {pluginManagerProfile} from '@remixproject/plugin-api' -import {LibraryProfile, Profile} from '@remixproject/plugin-utils' - +import React, { useEffect, useState } from 'react' +import { RemixPlugin } from './Client' +import { Logger } from './logger' +import { filePanelProfile } from '@remixproject/plugin-api' +import { filSystemProfile } from '@remixproject/plugin-api' +import { editorProfile } from '@remixproject/plugin-api' +import { settingsProfile } from '@remixproject/plugin-api' +import { networkProfile } from '@remixproject/plugin-api' +import { udappProfile } from '@remixproject/plugin-api' +import { compilerProfile } from '@remixproject/plugin-api' +import { contentImportProfile } from '@remixproject/plugin-api' +import { windowProfile } from '@remixproject/plugin-api' +import { pluginManagerProfile } from '@remixproject/plugin-api' +import { LibraryProfile, Profile } from '@remixproject/plugin-utils' +import './app.css' export const dGitProfile: LibraryProfile = { name: 'dgitApi', methods: ['status', 'log', 'commit', 'add', 'branches'], } -import './app.css' + +export const topbarProfile: LibraryProfile = { + name: 'topbar', + methods: ['getWorkspaces'], +} const client = new RemixPlugin() @@ -38,16 +42,17 @@ function App() { compilerProfile, udappProfile, contentImportProfile, - windowProfile + windowProfile, + topbarProfile ]) - const handleChange = ({target}: any) => { + const handleChange = ({ target }: any) => { setPayload(target.value) } useEffect(() => { client.onload(async () => { - const customProfiles = ['menuicons', 'tabs', 'solidityUnitTesting', 'hardhat-provider', 'notification'] + const customProfiles = ['menuicons', 'tabs', 'solidityUnitTesting', 'hardhat-provider', 'notification', 'topbar'] client.testCommand = async (data: any) => { methodLog(data) @@ -123,7 +128,7 @@ function App() { {profiles.map((profile: Profile) => { const methods = profile.methods.map((method: string) => { return ( - ) @@ -140,9 +145,9 @@ function App() { return (
-

+
{methods} -

+
{events ? : null} {events}
diff --git a/apps/remix-ide-e2e/src/tests/plugin_api.ts b/apps/remix-ide-e2e/src/tests/plugin_api.ts index ad55439ce6d..1c189c23f48 100644 --- a/apps/remix-ide-e2e/src/tests/plugin_api.ts +++ b/apps/remix-ide-e2e/src/tests/plugin_api.ts @@ -61,7 +61,9 @@ const clearPayLoad = async (browser: NightwatchBrowser) => { const clickButton = async (browser: NightwatchBrowser, buttonText: string, waitResult: boolean = true) => { // eslint-disable-line return new Promise((resolve) => { - browser.useXpath().waitForElementVisible(`//*[@data-id='${buttonText}']`).pause(100) + browser + .useXpath() + .waitForElementVisible(`//*[@data-id='${buttonText}']`).pause(100) .click(`//*[@data-id='${buttonText}']`, async () => { await checkForAcceptAndRemember(browser) if (waitResult) { @@ -173,7 +175,7 @@ module.exports = { // UDAPP 'Should get accounts #group1': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'udapp:getAccounts', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', null, null) + await clickAndCheckLog(browser, 'udapp-getAccounts', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', null, null) }, 'Should select another provider #group1': async function (browser: NightwatchBrowser) { @@ -185,7 +187,7 @@ module.exports = { .clickLaunchIcon('localPlugin') .useXpath() .frame(0) - await clickAndCheckLog(browser, 'udapp:setEnvironmentMode', null, null, { context: 'vm-berlin' }) + await clickAndCheckLog(browser, 'udapp-setEnvironmentMode', null, null, { context: 'vm-berlin' }) await browser .frameParent() .useCss() @@ -199,7 +201,7 @@ module.exports = { // context menu item 'Should create context menu item #group1': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:registerContextMenuItem', null, null, { + await clickAndCheckLog(browser, 'filePanel-registerContextMenuItem', null, null, { id: 'localPlugin', name: 'testCommand', label: 'testCommand', @@ -228,11 +230,11 @@ module.exports = { // FILESYSTEM 'Should get current workspace #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'default_workspace', isLocalhost: false, absolutePath: '.workspaces/default_workspace' }, null, null) + await clickAndCheckLog(browser, 'filePanel-getCurrentWorkspace', { name: 'default_workspace', isLocalhost: false, absolutePath: '.workspaces/default_workspace' }, null, null) }, 'Should get current files #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:readdir', { + await clickAndCheckLog(browser, 'fileManager-readdir', { contracts: { isDirectory: true }, scripts: { isDirectory: true }, tests: { isDirectory: true }, @@ -241,59 +243,59 @@ module.exports = { }, null, '/') }, 'Should throw error on current file #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'Error from IDE : Error: No such file or directory No file selected', null, null) + await clickAndCheckLog(browser, 'fileManager-getCurrentFile', 'Error from IDE : Error: No such file or directory No file selected', null, null) }, 'Should open readme.txt #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:open', null, { event: 'currentFileChanged', args: ['README.txt']}, 'README.txt') + await clickAndCheckLog(browser, 'fileManager-open', null, { event: 'currentFileChanged', args: ['README.txt']}, 'README.txt') }, 'Should have current file #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'README.txt', null, null) + await clickAndCheckLog(browser, 'fileManager-getCurrentFile', 'README.txt', null, null) }, 'Should create dir #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:mkdir', null, null, 'testdir') - await clickAndCheckLog(browser, 'fileManager:readdir', 'testdir', null, '/') + await clickAndCheckLog(browser, 'fileManager-mkdir', null, null, 'testdir') + await clickAndCheckLog(browser, 'fileManager-readdir', 'testdir', null, '/') }, 'Should get file #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:getFile', 'REMIX DEFAULT WORKSPACE', null, 'README.txt') + await clickAndCheckLog(browser, 'fileManager-getFile', 'REMIX DEFAULT WORKSPACE', null, 'README.txt') }, 'Should close all files #group7': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:closeAllFiles', null, { event: 'noFileSelected', args: []}, null) + await clickAndCheckLog(browser, 'fileManager-closeAllFiles', null, { event: 'noFileSelected', args: []}, null) }, 'Should switch to file #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:switchFile', null, { event: 'currentFileChanged', args: ['contracts/1_Storage.sol']}, 'contracts/1_Storage.sol') - await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'contracts/1_Storage.sol', null, null) - await clickAndCheckLog(browser, 'fileManager:switchFile', null, { event: 'currentFileChanged', args: ['README.txt']}, 'README.txt') - await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'README.txt', null, null) + await clickAndCheckLog(browser, 'fileManager-switchFile', null, { event: 'currentFileChanged', args: ['contracts/1_Storage.sol']}, 'contracts/1_Storage.sol') + await clickAndCheckLog(browser, 'fileManager-getCurrentFile', 'contracts/1_Storage.sol', null, null) + await clickAndCheckLog(browser, 'fileManager-switchFile', null, { event: 'currentFileChanged', args: ['README.txt']}, 'README.txt') + await clickAndCheckLog(browser, 'fileManager-getCurrentFile', 'README.txt', null, null) }, 'Should write to file #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:writeFile', null, { event: 'fileSaved', args: ['README.txt']}, ['README.txt', 'test']) + await clickAndCheckLog(browser, 'fileManager-writeFile', null, { event: 'fileSaved', args: ['README.txt']}, ['README.txt', 'test']) browser.pause(4000, async () => { - await clickAndCheckLog(browser, 'fileManager:getFile', 'test', null, 'README.txt') + await clickAndCheckLog(browser, 'fileManager-getFile', 'test', null, 'README.txt') }) }, 'Should set file #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:setFile', null, { event: 'fileAdded', args: ['new.sol']}, ['new.sol', 'test']) - await clickAndCheckLog(browser, 'fileManager:readFile', 'test', null, 'new.sol') + await clickAndCheckLog(browser, 'fileManager-setFile', null, { event: 'fileAdded', args: ['new.sol']}, ['new.sol', 'test']) + await clickAndCheckLog(browser, 'fileManager-readFile', 'test', null, 'new.sol') }, 'Should write to new file #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:writeFile', null, { event: 'fileAdded', args: ['testing.txt']}, ['testing.txt', 'test']) - await clickAndCheckLog(browser, 'fileManager:readFile', 'test', null, 'testing.txt') + await clickAndCheckLog(browser, 'fileManager-writeFile', null, { event: 'fileAdded', args: ['testing.txt']}, ['testing.txt', 'test']) + await clickAndCheckLog(browser, 'fileManager-readFile', 'test', null, 'testing.txt') }, 'Should rename file #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'fileManager:rename', null, null, ['testing.txt', 'testrename.txt']) - await clickAndCheckLog(browser, 'fileManager:readFile', 'test', null, 'testrename.txt') + await clickAndCheckLog(browser, 'fileManager-rename', null, null, ['testing.txt', 'testrename.txt']) + await clickAndCheckLog(browser, 'fileManager-readFile', 'test', null, 'testrename.txt') }, 'Should create empty workspace #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, ['emptyworkspace', true]) - await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'emptyworkspace', isLocalhost: false, absolutePath: '.workspaces/emptyworkspace' }, null, null) - await clickAndCheckLog(browser, 'fileManager:readdir', {}, null, '/') + await clickAndCheckLog(browser, 'filePanel-createWorkspace', null, null, ['emptyworkspace', true]) + await clickAndCheckLog(browser, 'filePanel-getCurrentWorkspace', { name: 'emptyworkspace', isLocalhost: false, absolutePath: '.workspaces/emptyworkspace' }, null, null) + await clickAndCheckLog(browser, 'fileManager-readdir', {}, null, '/') }, 'Should create workspace #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, 'testspace') - await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'testspace', isLocalhost: false, absolutePath: '.workspaces/testspace' }, null, null) - await clickAndCheckLog(browser, 'fileManager:readdir', { + await clickAndCheckLog(browser, 'filePanel-createWorkspace', null, null, 'testspace') + await clickAndCheckLog(browser, 'filePanel-getCurrentWorkspace', { name: 'testspace', isLocalhost: false, absolutePath: '.workspaces/testspace' }, null, null) + await clickAndCheckLog(browser, 'fileManager-readdir', { contracts: { isDirectory: true }, scripts: { isDirectory: true }, tests: { isDirectory: true }, @@ -302,10 +304,10 @@ module.exports = { }, null, '/') }, 'Should get all workspaces #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{ name:"default_workspace",isGitRepo:false,hasGitSubmodules:false,isGist:null }, { name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null }, { name:"testspace",isGitRepo:false,hasGitSubmodules:false,isGist:null }], null, null) + await clickAndCheckLog(browser, 'topbar-getWorkspaces', [{ name:"default_workspace",isGitRepo:false,hasGitSubmodules:false,isGist:null }, { name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null }, { name:"testspace",isGitRepo:false,hasGitSubmodules:false,isGist:null }], null, null) }, 'Should have set workspace event #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, { event: 'setWorkspace', args: [{ name: 'newspace', isLocalhost: false }]}, 'newspace') + await clickAndCheckLog(browser, 'filePanel-createWorkspace', null, { event: 'setWorkspace', args: [{ name: 'newspace', isLocalhost: false }]}, 'newspace') }, 'Should have event when switching workspace #group2': async function (browser: NightwatchBrowser) { // @ts-ignore @@ -315,76 +317,76 @@ module.exports = { }, 'Should rename workspace #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:renameWorkspace', null, null, ['default_workspace', 'renamed']) - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{ name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"testspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"newspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"renamed",isGitRepo:false,hasGitSubmodules:false,isGist:null }], null, null) + await clickAndCheckLog(browser, 'filePanel-renameWorkspace', null, null, ['default_workspace', 'renamed']) + await clickAndCheckLog(browser, 'topbar-getWorkspaces', [{ name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"testspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"newspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"renamed",isGitRepo:false,hasGitSubmodules:false,isGist:null }], null, null) }, 'Should delete workspace #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:deleteWorkspace', null, null, ['testspace']) - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{ name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"newspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"renamed",isGitRepo:false,hasGitSubmodules:false,isGist:null }], null, null) + await clickAndCheckLog(browser, 'filePanel-deleteWorkspace', null, null, ['testspace']) + await clickAndCheckLog(browser, 'topbar-getWorkspaces', [{ name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"newspace",isGitRepo:false,hasGitSubmodules:false,isGist:null },{ name:"renamed",isGitRepo:false,hasGitSubmodules:false,isGist:null }], null, null) }, // DGIT 'Should have changes on new workspace #group3': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, 'dgit') - await clickAndCheckLog(browser, 'dgitApi:status', [[".prettierrc.json",0,2,0], ["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,0],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null) + await clickAndCheckLog(browser, 'filePanel-createWorkspace', null, null, 'dgit') + await clickAndCheckLog(browser, 'dgitApi-status', [[".prettierrc.json",0,2,0], ["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,0],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null) }, 'Should stage contract #group3': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'dgitApi:add', null, null, { + await clickAndCheckLog(browser, 'dgitApi-add', null, null, { filepath: 'contracts/1_Storage.sol' }) - await clickAndCheckLog(browser, 'dgitApi:status', [[".prettierrc.json",0,2,0],["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,2],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null) + await clickAndCheckLog(browser, 'dgitApi-status', [[".prettierrc.json",0,2,0],["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,2],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null) }, 'Should commit changes #group3': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'dgitApi:commit', null, null, { author: { name: 'Remix', email: 'Remix' }, message: 'commit-message' }) - await clickAndCheckLog(browser, 'dgitApi:log', 'commit-message', null, null) + await clickAndCheckLog(browser, 'dgitApi-commit', null, null, { author: { name: 'Remix', email: 'Remix' }, message: 'commit-message' }) + await clickAndCheckLog(browser, 'dgitApi-log', 'commit-message', null, null) }, 'Should have git log #group3': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'dgitApi:log', 'commit-message', null, null) + await clickAndCheckLog(browser, 'dgitApi-log', 'commit-message', null, null) }, 'Should have branches #group3': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'dgitApi:branches', [{ name: 'main' }], null, null) + await clickAndCheckLog(browser, 'dgitApi-branches', [{ name: 'main' }], null, null) }, // resolver 'Should resolve url #group4': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'contentImport:resolve', '# Remix Project', null, 'https://github.com/ethereum/remix-project/blob/master/README.md') + await clickAndCheckLog(browser, 'contentImport-resolve', '# Remix Project', null, 'https://github.com/ethereum/remix-project/blob/master/README.md') }, 'Should resolve and save url #group4': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'contentImport:resolveAndSave', '# Remix Project', { event: 'fileAdded', args: ['.deps/github/ethereum/remix-project/README.md']}, 'https://github.com/ethereum/remix-project/blob/master/README.md') + await clickAndCheckLog(browser, 'contentImport-resolveAndSave', '# Remix Project', { event: 'fileAdded', args: ['.deps/github/ethereum/remix-project/README.md']}, 'https://github.com/ethereum/remix-project/blob/master/README.md') }, // UNIT TESTING 'Should activate solidityUnitTesting #group5': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'manager:activatePlugin', null, null, 'solidityUnitTesting') + await clickAndCheckLog(browser, 'manager-activatePlugin', null, null, 'solidityUnitTesting') browser.frameParent() assertPluginIsActive(browser, 'solidityUnitTesting', true) // @ts-ignore browser.frame(0) - await clickAndCheckLog(browser, 'manager:isActive', true, null, 'solidityUnitTesting') + await clickAndCheckLog(browser, 'manager-isActive', true, null, 'solidityUnitTesting') }, 'Should test from path with solidityUnitTesting #group5': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'solidityUnitTesting:testFromPath', '"totalPassing":2,"totalFailing":0', null, 'tests/Ballot_test.sol') + await clickAndCheckLog(browser, 'solidityUnitTesting-testFromPath', '"totalPassing":2,"totalFailing":0', null, 'tests/Ballot_test.sol') }, 'Should deactivate solidityUnitTesting #group5': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'manager:deactivatePlugin', null, null, 'solidityUnitTesting') + await clickAndCheckLog(browser, 'manager-deactivatePlugin', null, null, 'solidityUnitTesting') browser.frameParent() assertPluginIsActive(browser, 'solidityUnitTesting', false) // @ts-ignore browser.frame(0) - await clickAndCheckLog(browser, 'manager:isActive', false, null, 'solidityUnitTesting') + await clickAndCheckLog(browser, 'manager-isActive', false, null, 'solidityUnitTesting') }, // COMPILER 'Should compile a file #group6': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'solidity:compile', null, null, 'contracts/1_Storage.sol') + await clickAndCheckLog(browser, 'solidity-compile', null, null, 'contracts/1_Storage.sol') browser.pause(5000, async () => { - await clickAndCheckLog(browser, 'solidity:compile', null, 'compilationFinished', null) + await clickAndCheckLog(browser, 'solidity-compile', null, 'compilationFinished', null) }) }, 'Should get compilationresults #group6': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'solidity:getCompilationResult', 'contracts/1_Storage.sol', null, null) + await clickAndCheckLog(browser, 'solidity-getCompilationResult', 'contracts/1_Storage.sol', null, null) }, // PROVIDER @@ -410,7 +412,7 @@ module.exports = { params: [] } const result = '{"jsonrpc":"2.0","result":true,"id":9999}' - await clickAndCheckLog(browser, 'hardhat-provider:sendAsync', result, null, request) + await clickAndCheckLog(browser, 'hardhat-provider-sendAsync', result, null, request) }) }, @@ -445,8 +447,8 @@ module.exports = { // @ts-ignore .frame(0) .perform(async () => { - await clickAndCheckLog(browser, 'notification:toast', null, null, 'message toast from local plugin', false) // create a toast on behalf of the localplugin - await clickAndCheckLog(browser, 'notification:alert', null, null, { message: 'message from local plugin', id: 'test_id_1_local_plugin' }, false) // create an alert on behalf of the localplugin + await clickAndCheckLog(browser, 'notification-toast', null, null, 'message toast from local plugin', false) // create a toast on behalf of the localplugin + await clickAndCheckLog(browser, 'notification-alert', null, null, { message: 'message from local plugin', id: 'test_id_1_local_plugin' }, false) // create an alert on behalf of the localplugin }) .frameParent() .useCss() diff --git a/apps/remix-ide-e2e/src/tests/script-runner.test.ts b/apps/remix-ide-e2e/src/tests/script-runner.test.ts index 82edc69af72..c5a81381aea 100644 --- a/apps/remix-ide-e2e/src/tests/script-runner.test.ts +++ b/apps/remix-ide-e2e/src/tests/script-runner.test.ts @@ -20,9 +20,9 @@ const tests = { browser .waitForElementVisible('*[data-id="verticalIconsKindfilePanel"]') .click('*[data-id="verticalIconsKindfilePanel"]') - .waitForElementVisible('*[data-id="treeViewDivtreeViewItemscripts"]') + .waitForElementVisible('*[data-id="treeViewDivtreeViewItemscripts/deploy_with_ethers.ts"]') // .click('*[data-id="treeViewDivtreeViewItemscripts"]') - .pause(2000) + // .pause(3000) .waitForElementVisible('*[data-id="treeViewDivtreeViewItemscripts/deploy_with_ethers.ts"]') .click('*[data-id="treeViewDivtreeViewItemscripts/deploy_with_ethers.ts"]') .waitForElementVisible('*[data-id="run-script-dropdown-trigger"]') @@ -76,7 +76,6 @@ const tests = { .waitForElementPresent('*[data-id="create-semaphore"]') .scrollAndClick('*[data-id="create-semaphore"]') .modalFooterOKClick('TemplatesSelection') - .pause() .waitForElementVisible('*[data-id="treeViewLitreeViewItemcircuits/semaphore.circom"]') .waitForElementVisible({ locateStrategy: 'xpath', @@ -95,6 +94,7 @@ const tests = { .waitForElementVisible('*[data-id="workspacesSelect"]') .click('*[data-id="workspacesSelect"]') .click('*[data-id="workspacecreate"]') + .click('*[data-id="workspacesSelect"]') .waitForElementPresent('*[data-id="create-introToEIP7702"]') .scrollAndClick('*[data-id="create-introToEIP7702"]') .modalFooterOKClick('TemplatesSelection') @@ -113,9 +113,17 @@ const tests = { 'reset to default after template': function (browser: NightwatchBrowser) { browser .refreshPage() - .openFile('scripts/deploy.ts') + .waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts"') + .click('*[data-id="treeViewLitreeViewItemscripts"') + .pause(1000) + .waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/deploy.ts"]') + .pause(1000) + .click('*[data-path="scripts/deploy.ts"]') + .pause(1000) .waitForElementVisible('*[data-id="run-script-dropdown-trigger"]') + .pause(1000) .click('*[data-id="run-script-dropdown-trigger"]') + .pause(1000) .click('*[data-id="open-script-configuration-menu-item"]') .waitForElementVisible('[data-id="sr-notloaded-default"]') .waitForElementVisible('[data-id="sr-loaded-ethers6"]') diff --git a/apps/remix-ide-e2e/src/tests/vyper_api.test.ts b/apps/remix-ide-e2e/src/tests/vyper_api.test.ts index 838aefaea8c..f604220fbaf 100644 --- a/apps/remix-ide-e2e/src/tests/vyper_api.test.ts +++ b/apps/remix-ide-e2e/src/tests/vyper_api.test.ts @@ -25,7 +25,7 @@ module.exports = { .waitForElementVisible('button[data-id="add-repository"]') .click('button[data-id="add-repository"]') .frameParent() - .clickLaunchIcon('filePanel') + // .clickLaunchIcon('filePanel') .waitForElementVisible({ selector: "//*[@data-id='workspacesSelect' and contains(.,'vyper')]", locateStrategy: 'xpath', diff --git a/apps/remix-ide-e2e/src/tests/workspace.test.ts b/apps/remix-ide-e2e/src/tests/workspace.test.ts index 30f29f8f165..84d8e7627aa 100644 --- a/apps/remix-ide-e2e/src/tests/workspace.test.ts +++ b/apps/remix-ide-e2e/src/tests/workspace.test.ts @@ -124,9 +124,10 @@ module.exports = { .scrollAndClick('*[data-id="modalDialogCustomPromptTextCreate"]') .setValue('*[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_blank') // eslint-disable-next-line dot-notation - .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_blank' }) + // .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_blank' }) .click('*[data-id="TemplatesSelection-modal-footer-ok-react"]') - .pause(100) + .pause() + // .pause(100) .currentWorkspaceIs('workspace_blank') .waitForElementPresent('*[data-id="treeViewUltreeViewMenu"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItem.prettierrc.json"]') @@ -472,14 +473,13 @@ module.exports = { .currentWorkspaceIs('workspace_name') }, - 'Should rename a workspace #group1': !function (browser: NightwatchBrowser) { - const selector = 'a[data-id="dropdown-item-workspace_name"] + div[data-id="workspacesubMenuIcon"]' + 'Should rename a workspace #group1': function (browser: NightwatchBrowser) { browser .waitForElementPresent('*[data-id="workspacesSelect"]') .click('*[data-id="workspacesSelect"]') .waitForElementVisible('*[data-id="dropdown-item-workspace_name"]') - .waitForElementVisible(selector) - .click(selector) + .waitForElementVisible('*[data-id="workspacesubMenuIcon"]') + .click('*[data-id="workspacesubMenuIcon"]') .click('*[data-id="workspacesubMenuRename"]') // rename workspace_name .pause(500) .waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]') @@ -492,28 +492,30 @@ module.exports = { .switchWorkspace('workspace_name_1') .pause(2000) .currentWorkspaceIs('workspace_name_1') - .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .switchWorkspace('workspace_name_renamed') .pause(2000) .currentWorkspaceIs('workspace_name_renamed') .waitForElementVisible('*[data-id="treeViewDivtreeViewItemtests"]') }, - 'Should delete a workspace #group1': !function (browser: NightwatchBrowser) { - const selector = 'a[data-id="dropdown-item-workspace_name_1"] + div[data-id="workspacesubMenuIcon"]' + 'Should delete a workspace #group1': function (browser: NightwatchBrowser) { + const selector = 'a[data-id="dropdown-item-workspace_name_1"] + div [data-id="workspacesubMenuIcon"]' browser - .switchWorkspace('workspace_name_1') - .waitForElementPresent('*[data-id="workspacesSelect"]') .click('*[data-id="workspacesSelect"]') .waitForElementVisible(`[data-id="dropdown-item-workspace_name_1"]`) .waitForElementVisible(selector) .click(selector) .click('*[data-id="workspacesubMenuDelete"]') // delete workspace_name_1 - .pause(500) .waitForElementVisible('*[data-id="topbarModalModalDialogModalFooter-react"]') .click('*[data-id="topbarModalModalDialogModalFooter-react"] .modal-ok') .waitForElementVisible('*[data-id="workspacesSelect"]') .click('*[data-id="workspacesSelect"]') + .waitForElementVisible('*[data-id="dropdown-item-workspace_name"]') + .click('*[data-id="dropdown-item-workspace_name"]') + .waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]') + .click('*[data-id="treeViewLitreeViewItemcontracts"]') + .waitForElementVisible('*[data-id="workspacesSelect"]') + .click('*[data-id="workspacesSelect"]') .waitForElementNotPresent(`[data-id="dropdown-item-workspace_name_1"]`) .end() }, diff --git a/apps/remix-ide/src/app/components/top-bar.tsx b/apps/remix-ide/src/app/components/top-bar.tsx index 921ca7adce0..7a70479f7cc 100644 --- a/apps/remix-ide/src/app/components/top-bar.tsx +++ b/apps/remix-ide/src/app/components/top-bar.tsx @@ -13,6 +13,8 @@ import { HOME_TAB_NEW_UPDATES } from 'libs/remix-ui/home-tab/src/lib/components/ import axios from 'axios' import { UpdateInfo } from 'libs/remix-ui/home-tab/src/lib/components/types/carouselTypes' import { GitPlugin } from '../plugins/git' +import { createWorkspace, deleteWorkspace, getWorkspaces, renameWorkspace, WorkspaceType } from 'libs/remix-ui/workspace/src/lib/actions' +import { Registry } from '@remix-project/remix-lib' const TopBarProfile = { name: 'topbar', @@ -20,8 +22,8 @@ const TopBarProfile = { description: '', version: packageJson.version, icon: '', - methods: [], - events: [] + methods: ['getWorkspaces', 'createWorkspace', 'renameWorkspace', 'deleteWorkspace', 'getCurrentWorkspace', 'setWorkspace'], + events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'], } export class Topbar extends Plugin { @@ -32,12 +34,18 @@ export class Topbar extends Plugin { topbarExpandPath: string filePanel: FilePanel git: GitPlugin - workspaces: WorkspaceMetadata[] + workspaces: WorkspaceMetadata[] | WorkspaceType[] currentWorkspaceMetadata: WorkspaceMetadata + registry: Registry + fileProviders: any + fileManager: any constructor(filePanel: FilePanel, git: GitPlugin) { super(TopBarProfile) this.filePanel = filePanel + this.registry = Registry.getInstance() + this.fileProviders = this.registry.get('fileproviders').api + this.fileManager = this.registry.get('filemanager').api this.git = git this.workspaces = [] this.currentWorkspaceMetadata = null @@ -51,18 +59,99 @@ export class Topbar extends Plugin { } + getCurrentWorkspace() { + return this.currentWorkspaceMetadata + } + async getWorkspaces() { - this.on('filePanel', 'setWorkspaces', (workspaces) => { - this.workspaces = workspaces - }) + this.workspaces = await getWorkspaces() return this.workspaces } + async createWorkspace(workspaceName, workspaceTemplateName, isEmpty) { + // return new Promise((resolve, reject) => { + // this.emit('createWorkspaceReducerEvent', workspaceName, workspaceTemplateName, isEmpty, (err, data) => { + // if (err) reject(err) + // else resolve(data || true) + // }) + // }) + try { + await createWorkspace(workspaceName, workspaceTemplateName, isEmpty) + this.emit('workspaceCreated', workspaceName, workspaceTemplateName, isEmpty) + } catch (error) { + console.error('Error creating workspace:', error) + } + } + + async renameWorkspace(oldName, workspaceName) { + // return new Promise((resolve, reject) => { + // this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => { + // if (err) reject(err) + // else resolve(data || true) + // }) + // }) + try { + await renameWorkspace(oldName, workspaceName) + this.emit('workspaceRenamed', oldName, workspaceName) + } catch (error) { + console.error('Error renaming workspace:', error) + } + } + + async deleteWorkspace(workspaceName) { + // return new Promise((resolve, reject) => { + // this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => { + // if (err) reject(err) + // else resolve(data || true) + // }) + // }) + try { + await deleteWorkspace(workspaceName) + this.emit('workspaceDeleted', workspaceName) + } catch (error) { + console.error('Error deleting workspace:', error) + } + } + async getCurrentWorkspaceMetadata() { - while (!this.currentWorkspaceMetadata) { - await new Promise(resolve => setTimeout(resolve, 100)) - this.currentWorkspaceMetadata = await this.call('filePanel', 'getCurrentWorkspace') + this.currentWorkspaceMetadata = await this.fileManager.getCurrentWorkspace() + return this.currentWorkspaceMetadata + } + + setWorkspace(workspace) { + const workspaceProvider = this.fileProviders.workspace + const current = this.currentWorkspaceMetadata + this.currentWorkspaceMetadata = { + name: workspace.name, + isLocalhost: workspace.isLocalhost, + absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}`, } + if (this.currentWorkspaceMetadata.name !== current.name) { + this.saveRecent(workspace.name) + } + if (workspace.name !== ' - connect to localhost - ') { + localStorage.setItem('currentWorkspace', workspace.name) + } + this.emit('setWorkspace', workspace) + } + saveRecent(name: any) { + throw new Error('Method not implemented.') + } + + switchToWorkspace(workspaceName) { + this.emit('switchToWorkspace', workspaceName) + } + + workspaceRenamed(oldName, workspaceName) { + this.emit('workspaceRenamed', oldName, workspaceName) + } + + workspaceDeleted(workspace) { + this.emit('workspaceDeleted', workspace) + } + + workspaceCreated(workspace) { + this.emit('workspaceCreated', workspace) } async logInGithub () { diff --git a/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx b/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx index 7896e534708..25f755db482 100644 --- a/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx +++ b/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx @@ -308,6 +308,7 @@ export class TemplatesSelectionPlugin extends ViewPlugin { } }} className="btn btn-sm me-2 border border-primary" + data-template-name={item.name} > {isElectron() ? <>Create : 'Create'} diff --git a/libs/remix-api/src/lib/plugins/topbar-api.ts b/libs/remix-api/src/lib/plugins/topbar-api.ts new file mode 100644 index 00000000000..0fd67c69b7c --- /dev/null +++ b/libs/remix-api/src/lib/plugins/topbar-api.ts @@ -0,0 +1,21 @@ +import { StatusEvents } from "@remixproject/plugin-utils"; +import { WorkspaceMetadata } from "libs/remix-ui/workspace/src/lib/types"; + +export interface ITopbarApi { + events: { + setWorkspace: (workspace: WorkspaceMetadata) => void, + workspaceRenamed: (oldName: string, workspaceName: string) => void, + workspaceDeleted: (workspace: WorkspaceMetadata) => void, + workspaceCreated: (workspace: WorkspaceMetadata) => void, + } & StatusEvents + methods: { + getWorkspaces: () => Promise, + createWorkspace: (workspaceName: string, workspaceTemplateName: string, isEmpty: boolean) => Promise, + renameWorkspace: (oldName: string, workspaceName: string) => Promise, + deleteWorkspace: (workspaceName: string) => Promise, + getCurrentWorkspaceMetadata: () => Promise, + setWorkspace: (workspace: WorkspaceMetadata) => Promise, + switchToWorkspace: (workspaceName: string) => Promise, + getCurrentWorkspace: () => Promise, + } +} diff --git a/libs/remix-api/src/lib/remix-api.ts b/libs/remix-api/src/lib/remix-api.ts index be13b53a06f..df86e9840a2 100644 --- a/libs/remix-api/src/lib/remix-api.ts +++ b/libs/remix-api/src/lib/remix-api.ts @@ -20,6 +20,7 @@ import { IDgitPlugin } from "./plugins/dgitplugin-api" import { IPopupPanelAPI } from "./plugins/popuppanel-api" import { IDesktopClient } from "./plugins/desktop-client" import { IGitHubAuthHandlerApi } from "./plugins/githubAuthHandler-api" +import { ITopbarApi } from "./plugins/topbar-api" export interface ICustomRemixApi extends IRemixApi { popupPanel: IPopupPanelAPI @@ -32,6 +33,7 @@ export interface ICustomRemixApi extends IRemixApi { fileManager: IExtendedFileSystem isogit: IGitApi, terminal: IExtendedTerminalApi + topbar: ITopbarApi fs: IFs filePanel: IFilePanelApi sidePanel: ISidePanelApi diff --git a/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx b/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx index dc26a6a35fe..64063b220ba 100644 --- a/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx @@ -319,7 +319,7 @@ export function ContractGUI(props: ContractGUIProps) { : buttonOptions.title } > -
+
- {/* submenu toggle */} -
{ e.stopPropagation(); toggleSub(idx) }} - style={{ padding: '', cursor: 'pointer' }} - ref={subRefs[idx]} - data-id="workspacesubMenuIcon" - > - ⋮ -
- - {({ placement, arrowProps, show: _show, popper, ...overlayProps }) => ( -
setOpenSubmenuId(null)} > - { - renameCurrentWorkspace(item.name) - setCurrentMenuItemName(item.name) - setShowMain(false) - setOpenSub(null) - }} - onMouseDown={(e) => e.preventDefault()} - as={'button'} - data-id="workspacesubMenuRename" - > - - - - Rename - - {/* { - downloadCurrentWorkspace() - setCurrentMenuItemName(item.name) - setShowMain(false) - setOpenSub(null) - }} - onMouseDown={(e) => e.preventDefault()} - as={'button'} - data-id="workspacesubMenuDuplicate" - > - - - - Duplicate - */} - { - downloadCurrentWorkspace() - setCurrentMenuItemName(item.name) - setShowMain(false) - setOpenSub(null) - }} - onMouseDown={(e) => e.preventDefault()} - as={'button'} - data-id="workspacesubMenuDownload" - > - - - - Download - - - { - deleteCurrentWorkspace(item.name) - setShowMain(false) - setOpenSub(null) +
e.preventDefault()} - as={'button'} - data-id="workspacesubMenuDelete" + data-id="workspacesubMenuOverlay" > - - - - Delete - -
- )} -
-
- ))} +
+
+ + + +
+
+ + +
+ + ) + })} -
  • { - createWorkspace() - setShowMain(false) - setOpenSub(null) - }} - data-id="workspacecreate" - > - { - createWorkspace() - setShowMain(false) - setOpenSub(null) - }}> - +
    + { + createWorkspace() + setOpenSub(null) + }} + style={{ + backgroundColor: 'transparent', + color: 'inherit', + }} + > +
  • - - { - window.open('https://github.com/remix-project-org/remix-desktop/releases', '_blank') - setShowMain(false) - setOpenSub(null) - }}> - { + + + + { window.open('https://github.com/remix-project-org/remix-desktop/releases', '_blank') setShowMain(false) setOpenSub(null) }}> - + { + window.open('https://github.com/remix-project-org/remix-desktop/releases', '_blank') + setShowMain(false) + setOpenSub(null) + }}> + Download Remix Desktop - - - { - downloadWorkspaces() - setShowMain(false) - setOpenSub(null) - }}> - { + + + { downloadWorkspaces() setShowMain(false) setOpenSub(null) }}> - + { + downloadWorkspaces() + setShowMain(false) + setOpenSub(null) + }}> + Backup - - - { - restoreBackup() - setShowMain(false) - setOpenSub(null) - }}> - { + + + { restoreBackup() setShowMain(false) setOpenSub(null) }}> - + { + restoreBackup() + setShowMain(false) + setOpenSub(null) + }}> + Restore - - - { - connectToLocalhost() - setShowMain(false) - setOpenSub(null) - }}> - { + + + { connectToLocalhost() setShowMain(false) setOpenSub(null) }}> - + { + connectToLocalhost() + setShowMain(false) + setOpenSub(null) + }}> + Connect to Localhost - - -
  • { - deleteAllWorkspaces() - setShowMain(false) - setOpenSub(null) - }}> - { - deleteAllWorkspaces() - setShowMain(false) - setOpenSub(null) - }}> - + + + +
  • + + + + - ); -}; + ) +} diff --git a/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx b/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx index 18a9cd8fda0..0a1c806d629 100644 --- a/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx +++ b/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx @@ -479,7 +479,7 @@ export function RemixUiTopbar () { {currentReleaseVersion} -
    +