diff --git a/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts b/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts index a090735f45..26da9cf9c8 100644 --- a/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts +++ b/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts @@ -3,15 +3,15 @@ import EventEmitter from 'events' class CurrentWorkspaceIs extends EventEmitter { command (this: NightwatchBrowser, name: string): NightwatchBrowser { - this.api - .execute(function () { - const el = document.querySelector('select[data-id="workspacesSelect"]') as HTMLSelectElement - return el.value - }, [], (result) => { - console.log(result) - this.api.assert.equal(result.value, name) - this.emit('complete') - }) + const browser = this.api + + browser.getText('[data-id="workspacesSelect"]', function (result) { + browser.assert.equal(result.value, name) + }) + .perform((done) => { + done() + this.emit('complete') + }) return this } } diff --git a/apps/remix-ide-e2e/src/commands/switchWorkspace.ts b/apps/remix-ide-e2e/src/commands/switchWorkspace.ts new file mode 100644 index 0000000000..6219ddd218 --- /dev/null +++ b/apps/remix-ide-e2e/src/commands/switchWorkspace.ts @@ -0,0 +1,18 @@ +import { NightwatchBrowser } from 'nightwatch' +import EventEmitter from 'events' + +class switchWorkspace extends EventEmitter { + command (this: NightwatchBrowser, workspaceName: string): NightwatchBrowser { + this.api.waitForElementVisible('[data-id="workspacesSelect"]') + .click('[data-id="workspacesSelect"]') + .waitForElementVisible(`[data-id="dropdown-item-${workspaceName}"]`) + .click(`[data-id="dropdown-item-${workspaceName}"]`) + .perform((done) => { + done() + this.emit('complete') + }) + return this + } +} + +module.exports = switchWorkspace diff --git a/apps/remix-ide-e2e/src/tests/generalSettings.test.ts b/apps/remix-ide-e2e/src/tests/generalSettings.test.ts index ca54678a7b..a4932b05fd 100644 --- a/apps/remix-ide-e2e/src/tests/generalSettings.test.ts +++ b/apps/remix-ide-e2e/src/tests/generalSettings.test.ts @@ -41,7 +41,7 @@ module.exports = { .setValue('*[data-id="settingsTabGistAccessToken"]', '**********') .click('*[data-id="settingsTabSaveGistToken"]') .waitForElementVisible('*[data-shared="tooltipPopup"]', 5000) - .assert.containsText('*[data-shared="tooltipPopup"]', 'Access token has been saved') + .assert.containsText('*[data-shared="tooltipPopup"]', 'GitHub credentials updated') .pause(3000) }, @@ -59,7 +59,7 @@ module.exports = { .pause(1000) .click('*[data-id="settingsTabRemoveGistToken"]') .waitForElementVisible('*[data-shared="tooltipPopup"]', 5000) - .assert.containsText('*[data-shared="tooltipPopup"]', 'Access token removed') + .assert.containsText('*[data-shared="tooltipPopup"]', 'GitHub credentials removed') .assert.containsText('*[data-id="settingsTabGistAccessToken"]', '') }, diff --git a/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts b/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts index 3d04a3edfc..0914f6c2f5 100644 --- a/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts +++ b/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts @@ -81,7 +81,7 @@ module.exports = { // these are test data entries 'Should have a workspace_test #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) - .click('*[data-id="workspacesSelect"] option[value="workspace_test"]') + .switchWorkspace('workspace_test') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest_contracts"]') }, 'Should have a sol file with test data #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { @@ -103,7 +103,7 @@ module.exports = { }, 'Should have a empty workspace #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) - .click('*[data-id="workspacesSelect"] option[value="emptyspace"]') + .switchWorkspace('emptyspace') }, // end of test data entries 'Should load with all storage blocked #group4': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/plugin_api.ts b/apps/remix-ide-e2e/src/tests/plugin_api.ts index 1c21c849ca..f05a2fe130 100644 --- a/apps/remix-ide-e2e/src/tests/plugin_api.ts +++ b/apps/remix-ide-e2e/src/tests/plugin_api.ts @@ -298,25 +298,25 @@ module.exports = { }, null, null) }, 'Should get all workspaces #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['default_workspace', 'emptyworkspace', 'testspace'], null, null) + await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"default_workspace",isGitRepo:false}, {name:"emptyworkspace",isGitRepo:false}, {name:"testspace",isGitRepo:false}], 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') }, 'Should have event when switching workspace #group2': async function (browser: NightwatchBrowser) { // @ts-ignore - browser.frameParent().useCss().clickLaunchIcon('filePanel').click('*[data-id="workspacesSelect"] option[value="default_workspace"]').useXpath().click('//*[@data-id="verticalIconsKindlocalPlugin"]').frame(0, async () => { + browser.frameParent().useCss().clickLaunchIcon('filePanel').switchWorkspace('default_workspace').useXpath().click('//*[@data-id="verticalIconsKindlocalPlugin"]').frame(0, async () => { await clickAndCheckLog(browser, null, null, { event: 'setWorkspace', args: [{ name: 'default_workspace', isLocalhost: false }] }, null) }) }, 'Should rename workspace #group2': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'filePanel:renameWorkspace', null, null, ['default_workspace', 'renamed']) - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['emptyworkspace', 'testspace', 'newspace', 'renamed'], null, null) + await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false},{name:"testspace",isGitRepo:false},{name:"newspace",isGitRepo:false},{name:"renamed",isGitRepo:false}], null, null) }, 'Should delete workspace #group2': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'filePanel:deleteWorkspace', null, null, ['testspace']) - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['emptyworkspace', 'newspace', 'renamed'], null, null) + await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false},{name:"newspace",isGitRepo:false},{name:"renamed",isGitRepo:false}], null, null) }, // DGIT 'Should have changes on new workspace #group3': async function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 30a2913891..7e61167107 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -181,7 +181,7 @@ module.exports = { .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' }) .waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]') .execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() }) - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]') + .currentWorkspaceIs('workspace_new') .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_tests.sol"]') .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]') .openFile('.deps/remix-tests/remix_tests.sol') diff --git a/apps/remix-ide-e2e/src/tests/url.test.ts b/apps/remix-ide-e2e/src/tests/url.test.ts index a201b4ea36..00885338b4 100644 --- a/apps/remix-ide-e2e/src/tests/url.test.ts +++ b/apps/remix-ide-e2e/src/tests/url.test.ts @@ -29,6 +29,7 @@ module.exports = { .click('[for="autoCompile"]') // we set it too false again .click('[for="autoCompile"]') // back to True in the local storage .assert.containsText('*[data-id="compilerContainerCompileBtn"]', 'contract-76747f6e19.sol') + .clickLaunchIcon('filePanel') .currentWorkspaceIs('code-sample') .getEditorValue((content) => { browser.assert.ok(content && content.indexOf( @@ -57,6 +58,7 @@ module.exports = { .url('http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&url=https://github.com/ethereum/remix-project/blob/master/apps/remix-ide/contracts/app/solidity/mode.sol&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0') .refresh() // we do one reload for making sure we already have the default workspace .pause(5000) + .clickLaunchIcon('filePanel') .currentWorkspaceIs('code-sample') .getEditorValue((content) => { browser.assert.ok(content && content.indexOf( @@ -113,7 +115,7 @@ module.exports = { .url('http://127.0.0.1:8080/#optimize=false&runs=200&evmVersion=null&version=soljson-v0.6.12+commit.27d51765.js&url=https://raw.githubusercontent.com/EthVM/evm-source-verification/main/contracts/1/0x011e5846975c6463a8c6337eecf3cbf64e328884/input.json') .refresh() .pause(5000) - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="code-sample"]') + .switchWorkspace('code-sample') .openFile('@openzeppelin') .openFile('@openzeppelin/contracts') .openFile('@openzeppelin/contracts/access') diff --git a/apps/remix-ide-e2e/src/tests/workspace.test.ts b/apps/remix-ide-e2e/src/tests/workspace.test.ts index 95819b7598..aa0d2bbaf1 100644 --- a/apps/remix-ide-e2e/src/tests/workspace.test.ts +++ b/apps/remix-ide-e2e/src/tests/workspace.test.ts @@ -235,7 +235,7 @@ module.exports = { .pause(2000) .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .pause(2000) - .click('*[data-id="workspacesSelect"] option[value="workspace_name"]') + .switchWorkspace('workspace_name') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') }, @@ -249,23 +249,23 @@ module.exports = { .setValue('*[data-id="modalDialogCustomPromptTextRename"]', 'workspace_name_renamed') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') - .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .switchWorkspace('workspace_name_1') .pause(2000) .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') - .click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') + .switchWorkspace('workspace_name_renamed') .pause(2000) .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') }, 'Should delete a workspace #group1': function (browser: NightwatchBrowser) { browser - .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .switchWorkspace('workspace_name_1') .click('*[data-id="workspaceDelete"]') // delete workspace_name_1 .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .waitForElementVisible('[data-id="workspacesSelect"]') + .click('[data-id="workspacesSelect"]') + .waitForElementNotPresent(`[data-id="dropdown-item-workspace_name_1"]`) .end() }, diff --git a/apps/remix-ide-e2e/src/types/index.d.ts b/apps/remix-ide-e2e/src/types/index.d.ts index f17e6b17de..eccd50bad5 100644 --- a/apps/remix-ide-e2e/src/types/index.d.ts +++ b/apps/remix-ide-e2e/src/types/index.d.ts @@ -62,7 +62,8 @@ declare module 'nightwatch' { clearConsole (this: NightwatchBrowser): NightwatchBrowser clearTransactions (this: NightwatchBrowser): NightwatchBrowser getBrowserLogs (this: NightwatchBrowser): NightwatchBrowser - currentSelectedFileIs (name: string): NightwatchBrowser + currentSelectedFileIs (name: string): NightwatchBrowser, + switchWorkspace: (workspaceName: string) => NightwatchBrowser } export interface NightwatchBrowser { diff --git a/apps/remix-ide/src/app/files/dgitProvider.js b/apps/remix-ide/src/app/files/dgitProvider.js index 249640a0f3..8b5ce925c5 100644 --- a/apps/remix-ide/src/app/files/dgitProvider.js +++ b/apps/remix-ide/src/app/files/dgitProvider.js @@ -233,12 +233,11 @@ class DGitProvider extends Plugin { return this.calculateLocalStorage() } - async clone (input) { + async clone (input, workspaceName, workspaceExists = false) { const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.') if (!permission) return false if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.') - await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true) - + if (!workspaceExists) await this.call('filePanel', 'createWorkspace', workspaceName || `workspace_${Date.now()}`, true) const cmd = { url: input.url, singleBranch: input.singleBranch, @@ -249,9 +248,11 @@ class DGitProvider extends Plugin { } const result = await git.clone(cmd) - setTimeout(async () => { - await this.call('fileManager', 'refresh') - }, 1000) + if (!workspaceExists) { + setTimeout(async () => { + await this.call('fileManager', 'refresh') + }, 1000) + } return result } diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index e7ae4763df..44b604b8d4 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -19,7 +19,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', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'], + methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles', 'isGitRepo'], kind: 'file-system' } const errorMsg = { @@ -810,6 +810,13 @@ class FileManager extends Plugin { return provider.workspace } } + + async isGitRepo (directory: string): Promise { + const path = directory + '/.git' + const exists = await this.exists(path) + + return exists + } } module.exports = FileManager diff --git a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts index 71abad338d..d9fd9b03b9 100644 --- a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts +++ b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts @@ -47,6 +47,22 @@ export const createNonClashingNameAsync = async (name: string, fileManager, pref return name + counter + prefix + '.' + ext } +export const createNonClashingTitle = async (name: string, fileManager) => { + if (!name) name = 'Undefined' + let _counter + let exist = true + + do { + const isDuplicate = await fileManager.exists(name + (_counter || '')) + + if (isDuplicate) _counter = (_counter || 0) + 1 + else exist = false + } while (exist) + const counter = _counter || '' + + return name + counter +} + export const joinPath = (...paths) => { paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) if (paths.length === 1) return paths[0] diff --git a/libs/remix-ui/settings/src/lib/github-settings.tsx b/libs/remix-ui/settings/src/lib/github-settings.tsx new file mode 100644 index 0000000000..e7c9c3f2ce --- /dev/null +++ b/libs/remix-ui/settings/src/lib/github-settings.tsx @@ -0,0 +1,81 @@ +import { CopyToClipboard } from '@remix-ui/clipboard' +import React, { useEffect, useState } from 'react' +import { GithubSettingsProps } from '../types' + +export function GithubSettings (props: GithubSettingsProps) { + const [githubToken, setGithubToken] = useState("") + const [githubUserName, setGithubUsername] = useState("") + const [githubEmail, setGithubEmail] = useState("") + + useEffect(() => { + if (props.config) { + const githubToken = props.config.get('settings/gist-access-token') + const githubUserName = props.config.get('settings/github-user-name') + const githubEmail = props.config.get('settings/github-email') + + setGithubToken(githubToken) + setGithubUsername(githubUserName) + setGithubEmail(githubEmail) + } + }, [props.config]) + + const handleChangeTokenState = (event) => { + setGithubToken(event.target.value) + } + + const handleChangeUserNameState = (event) => { + setGithubUsername(event.target.value) + } + + const handleChangeEmailState = (event) => { + setGithubEmail(event.target.value) + } + + // api key settings + const saveGithubToken = () => { + props.saveTokenToast(githubToken, githubUserName, githubEmail) + } + + const removeToken = () => { + setGithubToken('') + setGithubUsername('') + setGithubEmail('') + props.removeTokenToast() + } + + return ( +
+
+
GitHub Credentials
+

Manage your GitHub credentials used to publish to Gist and retrieve GitHub contents.

+

Go to github token page (link below) to create a new token and save it in Remix. Make sure this token has only \'create gist\' permission.

+

https://github.com/settings/tokens

+
+ +
+ handleChangeTokenState(e)} value={ githubToken } /> +
+ +
+
+
+
+ +
+ handleChangeUserNameState(e)} value={ githubUserName } /> +
+
+
+ +
+ handleChangeEmailState(e)} value={ githubEmail } /> +
+ + +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index 2de3ecd67b..19655dd281 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -8,6 +8,7 @@ import { ethereumVM, generateContractMetadat, personal, textWrapEventAction, use import { initialState, toastInitialState, toastReducer, settingReducer } from './settingsReducer' import { Toaster } from '@remix-ui/toaster'// eslint-disable-line import { RemixUiThemeModule, ThemeModule} from '@remix-ui/theme-module' +import { GithubSettings } from './github-settings' /* eslint-disable-next-line */ export interface RemixUiSettingsProps { @@ -347,7 +348,19 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
{state.message ? : null} {generalConfig()} - {token('gist')} + { + saveTokenToast(props.config, dispatchToast, githubToken, "gist-access-token") + saveTokenToast(props.config, dispatchToast, githubUserName, "github-user-name") + saveTokenToast(props.config, dispatchToast, githubEmail, "github-email") + }} + removeTokenToast={() => { + removeTokenToast(props.config, dispatchToast, "gist-access-token") + removeTokenToast(props.config, dispatchToast, "github-user-name") + removeTokenToast(props.config, dispatchToast, "github-email") + }} + config={props.config} + /> {token('etherscan')} {swarmSettings()} {ipfsSettings()} diff --git a/libs/remix-ui/settings/src/lib/settingsAction.ts b/libs/remix-ui/settings/src/lib/settingsAction.ts index b7a7eddd95..fc3bed1f4d 100644 --- a/libs/remix-ui/settings/src/lib/settingsAction.ts +++ b/libs/remix-ui/settings/src/lib/settingsAction.ts @@ -43,12 +43,12 @@ export const useMatomoAnalytics = (config, checked, dispatch) => { export const saveTokenToast = (config, dispatch, tokenValue, key) => { config.set('settings/' + key, tokenValue) - dispatch({ type: 'save', payload: { message: 'Access token has been saved' } }) + dispatch({ type: 'save', payload: { message: 'GitHub credentials updated' } }) } export const removeTokenToast = (config, dispatch, key) => { config.set('settings/' + key, '') - dispatch({ type: 'removed', payload: { message: 'Access token removed' } }) + dispatch({ type: 'removed', payload: { message: 'GitHub credentials removed' } }) } export const saveSwarmSettingsToast = (config, dispatch, privateBeeAddress, postageStampId) => { diff --git a/libs/remix-ui/settings/src/types/index.ts b/libs/remix-ui/settings/src/types/index.ts new file mode 100644 index 0000000000..5b4af8dfc1 --- /dev/null +++ b/libs/remix-ui/settings/src/types/index.ts @@ -0,0 +1,12 @@ +export interface GithubSettingsProps { + saveTokenToast: (githubToken: string, githubUserName: string, githubEmail: string) => void, + removeTokenToast: () => void, + config: { + exists: (key: string) => boolean, + get: (key: string) => string, + set: (key: string, content: string) => void, + clear: () => void, + getUnpersistedProperty: (key: string) => void, + setUnpersistedProperty: (key: string, value: string) => void + } +} diff --git a/libs/remix-ui/tooltip-popup/.babelrc b/libs/remix-ui/tooltip-popup/.babelrc new file mode 100644 index 0000000000..ccae900be4 --- /dev/null +++ b/libs/remix-ui/tooltip-popup/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/remix-ui/tooltip-popup/.eslintrc.json b/libs/remix-ui/tooltip-popup/.eslintrc.json new file mode 100644 index 0000000000..50e59482cf --- /dev/null +++ b/libs/remix-ui/tooltip-popup/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/remix-ui/tooltip-popup/README.md b/libs/remix-ui/tooltip-popup/README.md new file mode 100644 index 0000000000..91bf0a3a7d --- /dev/null +++ b/libs/remix-ui/tooltip-popup/README.md @@ -0,0 +1,7 @@ +# remix-ui-tooltip-popup + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test remix-ui-tooltip-popup` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/remix-ui/tooltip-popup/src/index.ts b/libs/remix-ui/tooltip-popup/src/index.ts new file mode 100644 index 0000000000..cad02e8853 --- /dev/null +++ b/libs/remix-ui/tooltip-popup/src/index.ts @@ -0,0 +1 @@ +export * from './lib/tooltip-popup' diff --git a/libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.module.css b/libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.module.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.tsx b/libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.tsx new file mode 100644 index 0000000000..077ed1a167 --- /dev/null +++ b/libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.tsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react' +import { OverlayTrigger, Popover } from 'react-bootstrap' +import { TooltipPopupProps } from '../types' +import './tooltip-popup.module.css' + +const popover = (title?: string, content?: string | React.ReactNode) => ( + + { title || 'Tooltip' } + + { content } + + +) + +export function TooltipPopup(props: TooltipPopupProps) { + const [show, setShow] = useState(false) + + return ( + { + setShow(nextShow) + }}> + + + ) +} + +export default TooltipPopup diff --git a/libs/remix-ui/tooltip-popup/src/types/index.ts b/libs/remix-ui/tooltip-popup/src/types/index.ts new file mode 100644 index 0000000000..747c392c4c --- /dev/null +++ b/libs/remix-ui/tooltip-popup/src/types/index.ts @@ -0,0 +1,6 @@ +export interface TooltipPopupProps { + children?: React.ReactNode, + title?: string, + content?: string, + icon: string +} diff --git a/libs/remix-ui/tooltip-popup/tsconfig.json b/libs/remix-ui/tooltip-popup/tsconfig.json new file mode 100644 index 0000000000..8bd701c578 --- /dev/null +++ b/libs/remix-ui/tooltip-popup/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/tooltip-popup/tsconfig.lib.json b/libs/remix-ui/tooltip-popup/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/tooltip-popup/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/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts index 973eeb0820..7a5387474b 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -24,17 +24,20 @@ export type UrlParametersType = { url: string } -const basicWorkspaceInit = async (workspaces, workspaceProvider) => { +const basicWorkspaceInit = async (workspaces: { name: string; isGitRepo: boolean; }[], workspaceProvider) => { if (workspaces.length === 0) { await createWorkspaceTemplate('default_workspace', 'remixDefault') plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false }) - dispatch(setCurrentWorkspace('default_workspace')) + dispatch(setCurrentWorkspace({ name: 'default_workspace', isGitRepo: false })) await loadWorkspacePreset('remixDefault') } else { if (workspaces.length > 0) { - workspaceProvider.setWorkspace(workspaces[workspaces.length - 1]) - plugin.setWorkspace({ name: workspaces[workspaces.length - 1], isLocalhost: false }) - dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1])) + const workspace = workspaces[workspaces.length - 1] + const workspaceName = (workspace || {}).name + + workspaceProvider.setWorkspace(workspaceName) + plugin.setWorkspace({ name: workspaceName, isLocalhost: false }) + dispatch(setCurrentWorkspace(workspace)) } } } @@ -52,12 +55,12 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React. if (params.gist) { await createWorkspaceTemplate('gist-sample', 'gist-template') plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false }) - dispatch(setCurrentWorkspace('gist-sample')) + dispatch(setCurrentWorkspace({ name: 'gist-sample', isGitRepo: false })) await loadWorkspacePreset('gist-template') } else if (params.code || params.url) { await createWorkspaceTemplate('code-sample', 'code-template') plugin.setWorkspace({ name: 'code-sample', isLocalhost: false }) - dispatch(setCurrentWorkspace('code-sample')) + dispatch(setCurrentWorkspace({ name: 'code-sample', isGitRepo: false })) const filePath = await loadWorkspacePreset('code-template') plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath)) } else if (window.location.pathname && window.location.pathname !== '/') { @@ -95,7 +98,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React. foundOnNetworks.push(network.name) await createWorkspaceTemplate('etherscan-code-sample', 'code-template') plugin.setWorkspace({ name: 'etherscan-code-sample', isLocalhost: false }) - dispatch(setCurrentWorkspace('etherscan-code-sample')) + dispatch(setCurrentWorkspace({ name: 'etherscan-code-sample', isGitRepo: false })) let filePath count = count + (Object.keys(data.compilationTargets)).length for (filePath in data.compilationTargets) @@ -119,7 +122,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React. const content = response.data await createWorkspaceTemplate('github-code-sample', 'code-template') plugin.setWorkspace({ name: 'github-code-sample', isLocalhost: false }) - dispatch(setCurrentWorkspace('github-code-sample')) + dispatch(setCurrentWorkspace({ name: 'github-code-sample', isGitRepo: false })) await workspaceProvider.set(route, content) plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(route)) } else await basicWorkspaceInit(workspaces, workspaceProvider) diff --git a/libs/remix-ui/workspace/src/lib/actions/payload.ts b/libs/remix-ui/workspace/src/lib/actions/payload.ts index a0ed0850cb..b663f60cfe 100644 --- a/libs/remix-ui/workspace/src/lib/actions/payload.ts +++ b/libs/remix-ui/workspace/src/lib/actions/payload.ts @@ -1,13 +1,13 @@ import { action } from '../types' -export const setCurrentWorkspace = (workspace: string) => { +export const setCurrentWorkspace = (workspace: { name: string; isGitRepo: boolean; }) => { return { type: 'SET_CURRENT_WORKSPACE', payload: workspace } } -export const setWorkspaces = (workspaces: string[]) => { +export const setWorkspaces = (workspaces: { name: string; isGitRepo: boolean; }[]) => { return { type: 'SET_WORKSPACES', payload: workspaces @@ -125,7 +125,7 @@ export const createWorkspaceRequest = (promise: Promise) => { } } -export const createWorkspaceSuccess = (workspaceName: string) => { +export const createWorkspaceSuccess = (workspaceName: { name: string; isGitRepo: boolean; }) => { return { type: 'CREATE_WORKSPACE_SUCCESS', payload: workspaceName @@ -239,3 +239,21 @@ export const fsInitializationCompleted = () => { type: 'FS_INITIALIZATION_COMPLETED' } } + +export const cloneRepositoryRequest = () => { + return { + type: 'CLONE_REPOSITORY_REQUEST' + } +} + +export const cloneRepositorySuccess = () => { + return { + type: 'CLONE_REPOSITORY_SUCCESS' + } +} + +export const cloneRepositoryFailed = () => { + return { + type: 'CLONE_REPOSITORY_FAILED' + } +} diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index 1b73f63cc0..12aab6f93d 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -1,8 +1,8 @@ import React from 'react' import { bufferToHex, keccakFromString } from 'ethereumjs-util' import axios, { AxiosResponse } from 'axios' -import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload' -import { checkSlash, checkSpecialChars } from '@remix-ui/helper' +import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload' +import { checkSlash, checkSpecialChars, createNonClashingTitle } from '@remix-ui/helper' import { JSONStandardInput, WorkspaceTemplate } from '../types' import { QueryParams } from '@remix-project/remix-lib' @@ -42,13 +42,13 @@ export const addInputField = async (type: 'file' | 'folder', path: string, cb?: return promise } -export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record) => void) => { +export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record) => void, isGitRepo: boolean = false) => { await plugin.fileManager.closeAllFiles() const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName) dispatch(createWorkspaceRequest(promise)) promise.then(async () => { - dispatch(createWorkspaceSuccess(workspaceName)) + dispatch(createWorkspaceSuccess({ name: workspaceName, isGitRepo })) await plugin.setWorkspace({ name: workspaceName, isLocalhost: false }) await plugin.setWorkspaces(await getWorkspaces()) await plugin.workspaceCreated(workspaceName) @@ -254,8 +254,10 @@ export const switchToWorkspace = async (name: string) => { if (isActive) await plugin.call('manager', 'deactivatePlugin', 'remixd') await plugin.fileProviders.workspace.setWorkspace(name) await plugin.setWorkspace({ name, isLocalhost: false }) + const isGitRepo = await plugin.fileManager.isGitRepo() + dispatch(setMode('browser')) - dispatch(setCurrentWorkspace(name)) + dispatch(setCurrentWorkspace({ name, isGitRepo })) dispatch(setReadOnlyMode(false)) } } @@ -302,22 +304,69 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error, }) } -export const getWorkspaces = async (): Promise | undefined => { +export const getWorkspaces = async (): Promise<{name: string, isGitRepo: boolean}[]> | undefined => { try { - const workspaces: string[] = await new Promise((resolve, reject) => { + const workspaces: {name: string, isGitRepo: boolean}[] = await new Promise((resolve, reject) => { const workspacesPath = plugin.fileProviders.workspace.workspacesPath plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => { if (error) { return reject(error) } - resolve(Object.keys(items) + Promise.all(Object.keys(items) .filter((item) => items[item].isDirectory) - .map((folder) => folder.replace(workspacesPath + '/', ''))) + .map(async (folder) => { + const isGitRepo: boolean = await plugin.fileProviders.browser.exists('/' + folder + '/.git') + return { + name: folder.replace(workspacesPath + '/', ''), + isGitRepo + } + })).then(workspacesList => resolve(workspacesList)) }) }) - await plugin.setWorkspaces(workspaces) return workspaces - } catch (e) {} + } catch (e) {} +} + +export const cloneRepository = async (url: string) => { + const config = plugin.registry.get('config').api + const token = config.get('settings/gist-access-token') + const repoConfig = { url, token } + const urlArray = url.split('/') + let repoName = urlArray.length > 0 ? urlArray[urlArray.length - 1] : '' + + try { + repoName = await createNonClashingTitle(repoName, plugin.fileManager) + await createWorkspace(repoName, 'blank', true, null, true) + const promise = plugin.call('dGitProvider', 'clone', repoConfig, repoName, true) + + dispatch(cloneRepositoryRequest()) + promise.then(async () => { + const isActive = await plugin.call('manager', 'isActive', 'dgit') + + if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit') + await fetchWorkspaceDirectory(repoName) + dispatch(cloneRepositorySuccess()) + }).catch((e) => { + const cloneModal = { + id: 'cloneGitRepository', + title: 'Clone Git Repository', + message: 'An error occured: ' + e, + modalType: 'modal', + okLabel: 'OK', + okFn: async () => { + await deleteWorkspace(repoName) + dispatch(cloneRepositoryFailed()) + }, + hideFn: async () => { + await deleteWorkspace(repoName) + dispatch(cloneRepositoryFailed()) + } + } + plugin.call('notification', 'modal', cloneModal) + }) + } catch (e) { + dispatch(displayPopUp('An error occured: ' + e)) + } } diff --git a/libs/remix-ui/workspace/src/lib/components/custom-dropdown.tsx b/libs/remix-ui/workspace/src/lib/components/custom-dropdown.tsx new file mode 100644 index 0000000000..5f0696a554 --- /dev/null +++ b/libs/remix-ui/workspace/src/lib/components/custom-dropdown.tsx @@ -0,0 +1,42 @@ +// The forwardRef is important!! + +import React, { Ref } from "react" + +// Dropdown needs access to the DOM node in order to position the Menu +export const CustomToggle = React.forwardRef(({ children, onClick, icon, className = '' }: { children: React.ReactNode, onClick: (e) => void, icon: string, className: string }, ref: Ref) => ( + +)) + +// forwardRef again here! +// Dropdown needs access to the DOM of the Menu to measure it +export const CustomMenu = React.forwardRef( + ({ children, style, className, 'aria-labelledby': labeledBy }: { children: React.ReactNode, style?: React.CSSProperties, className: string, 'aria-labelledby'?: string }, ref: Ref) => { + return ( +
+
    + { + children + } +
+
+ ) + }, +) diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts index 792bed8188..b71dc9e373 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -4,7 +4,7 @@ import { BrowserState } from '../reducers/workspace' export const FileSystemContext = createContext<{ fs: BrowserState, - modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, + modal:(title: string | JSX.Element, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, dispatchInitWorkspace:() => Promise, dispatchFetchDirectory:(path: string) => Promise, dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise, @@ -29,5 +29,6 @@ export const FileSystemContext = createContext<{ dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise dispatchHandleExpandPath: (paths: string[]) => Promise, dispatchHandleDownloadFiles: () => Promise, - dispatchHandleRestoreBackup: () => Promise + dispatchHandleRestoreBackup: () => Promise, + dispatchCloneRepository: (url: string) => Promise }>(null) diff --git a/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css b/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css index 62a557f8cf..29fd578d26 100644 --- a/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css +++ b/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css @@ -62,4 +62,40 @@ .remixui_menuicon:hover { transform: scale(1.3); } + .remixui_cloneContainer { + display: flex; + align-items: center; + height: 32px; + } + + .remixui_cloneContainer input { + height: 32px; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + width: 250px; + font-size: 10px !important; + padding: .25rem; + } + + .remixui_menuicon .bs-popover-auto[x-placement^="bottom"] .popover-header::before, .bs-popover-bottom .popover-header::before { + border-bottom-color: var(--dark) !important + } + + .remixui_menuicon .bs-popover-auto[x-placement^="bottom"] > .arrow::after, .bs-popover-bottom > .arrow::after { + border-bottom-color: var(--dark) !important + } + + .custom-dropdown-items { + padding: 0.25rem 0.25rem; + border-radius: .25rem; + } + .custom-dropdown-items a { + border-radius: .25rem; + text-transform: none; + text-decoration: none; + font-weight: normal; + font-size: 0.875rem; + padding: 0.25rem 0.25rem; + width: auto; + } \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index fab8e652b5..86f4cccaf2 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileSystemContext } from '../contexts' import { browserReducer, browserInitialState } from '../reducers/workspace' -import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip } from '../actions' +import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip, cloneRepository } from '../actions' import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Workspace } from '../remix-ui-workspace' @@ -123,6 +123,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => { await restoreBackupZip() } + const dispatchCloneRepository = async (url: string) => { + await cloneRepository(url) + } + useEffect(() => { dispatchInitWorkspace() }, []) @@ -224,7 +228,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => { dispatchHandleClickFile, dispatchHandleExpandPath, dispatchHandleDownloadFiles, - dispatchHandleRestoreBackup + dispatchHandleRestoreBackup, + dispatchCloneRepository } return ( diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index 8d33cb226d..39f56a202f 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -8,13 +8,18 @@ interface Action { export interface BrowserState { browser: { currentWorkspace: string, - workspaces: string[], + workspaces: { + name: string; + isGitRepo: boolean; + }[], files: { [x: string]: Record }, expandPath: string[] isRequestingDirectory: boolean, isSuccessfulDirectory: boolean, isRequestingWorkspace: boolean, isSuccessfulWorkspace: boolean, + isRequestingCloning: boolean, + isSuccessfulCloning: boolean, error: string, contextMenu: { registeredMenuItems: action[], @@ -63,6 +68,8 @@ export const browserInitialState: BrowserState = { isSuccessfulDirectory: false, isRequestingWorkspace: false, isSuccessfulWorkspace: false, + isRequestingCloning: false, + isSuccessfulCloning: false, error: null, contextMenu: { registeredMenuItems: [], @@ -104,21 +111,21 @@ export const browserInitialState: BrowserState = { export const browserReducer = (state = browserInitialState, action: Action) => { switch (action.type) { case 'SET_CURRENT_WORKSPACE': { - const payload = action.payload as string - const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] + const payload = action.payload as { name: string; isGitRepo: boolean; } + const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] return { ...state, browser: { ...state.browser, - currentWorkspace: payload, + currentWorkspace: payload.name, workspaces: workspaces.filter(workspace => workspace) } } } case 'SET_WORKSPACES': { - const payload = action.payload as string[] + const payload = action.payload as { name: string; isGitRepo: boolean; }[] return { ...state, @@ -416,14 +423,14 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } case 'CREATE_WORKSPACE_SUCCESS': { - const payload = action.payload as string - const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] + const payload = action.payload as { name: string; isGitRepo: boolean; } + const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] return { ...state, browser: { ...state.browser, - currentWorkspace: payload, + currentWorkspace: payload.name, workspaces: workspaces.filter(workspace => workspace), isRequestingWorkspace: false, isSuccessfulWorkspace: true, @@ -446,14 +453,25 @@ export const browserReducer = (state = browserInitialState, action: Action) => { case 'RENAME_WORKSPACE': { const payload = action.payload as { oldName: string, workspaceName: string } - const workspaces = state.browser.workspaces.filter(name => name && (name !== payload.oldName)) + let renamedWorkspace + const workspaces = state.browser.workspaces.filter(({ name, isGitRepo }) => { + if (name && (name !== payload.oldName)) { + return true + } else { + renamedWorkspace = { + name: payload.workspaceName, + isGitRepo + } + return false + } + }) return { ...state, browser: { ...state.browser, currentWorkspace: payload.workspaceName, - workspaces: [...workspaces, payload.workspaceName], + workspaces: [...workspaces, renamedWorkspace], expandPath: [] } } @@ -461,7 +479,7 @@ export const browserReducer = (state = browserInitialState, action: Action) => { case 'DELETE_WORKSPACE': { const payload = action.payload as string - const workspaces = state.browser.workspaces.filter(name => name && (name !== payload)) + const workspaces = state.browser.workspaces.filter(({ name }) => name && (name !== payload)) return { ...state, @@ -592,6 +610,39 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } + case 'CLONE_REPOSITORY_REQUEST': { + return { + ...state, + browser: { + ...state.browser, + isRequestingCloning: true, + isSuccessfulCloning: false + } + } + } + + case 'CLONE_REPOSITORY_SUCCESS': { + return { + ...state, + browser: { + ...state.browser, + isRequestingCloning: false, + isSuccessfulCloning: true + } + } + } + + case 'CLONE_REPOSITORY_FAILED': { + return { + ...state, + browser: { + ...state.browser, + isRequestingCloning: false, + isSuccessfulCloning: false + } + } + } + case 'FS_INITIALIZATION_COMPLETED': { return { ...state, diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 98a80306bd..5302d907ed 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -1,7 +1,9 @@ import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line +import { Dropdown } from 'react-bootstrap' +import { CustomMenu, CustomToggle } from './components/custom-dropdown' import { FileExplorer } from './components/file-explorer' // eslint-disable-line -import './css/remix-ui-workspace.css' import { FileSystemContext } from './contexts' +import './css/remix-ui-workspace.css' const canUpload = window.File || window.FileReader || window.FileList || window.Blob @@ -9,10 +11,12 @@ export function Workspace () { const LOCALHOST = ' - connect to localhost - ' const NO_WORKSPACE = ' - none - ' const [currentWorkspace, setCurrentWorkspace] = useState(NO_WORKSPACE) + const [selectedWorkspace, setSelectedWorkspace] = useState<{ name: string, isGitRepo: boolean}>(null) const global = useContext(FileSystemContext) const workspaceRenameInput = useRef() const workspaceCreateInput = useRef() const workspaceCreateTemplateInput = useRef() + const cloneUrlRef = useRef() useEffect(() => { resetFocus() @@ -30,15 +34,21 @@ export function Workspace () { }, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode]) useEffect(() => { - if (global.fs.browser.currentWorkspace && !global.fs.browser.workspaces.includes(global.fs.browser.currentWorkspace)) { + if (global.fs.browser.currentWorkspace && !global.fs.browser.workspaces.find(({ name }) => name === global.fs.browser.currentWorkspace)) { if (global.fs.browser.workspaces.length > 0) { - switchWorkspace(global.fs.browser.workspaces[global.fs.browser.workspaces.length - 1]) + switchWorkspace(global.fs.browser.workspaces[global.fs.browser.workspaces.length - 1].name) } else { switchWorkspace(NO_WORKSPACE) } } }, [global.fs.browser.workspaces]) + useEffect(() => { + const workspace = global.fs.browser.workspaces.find(workspace => workspace.name === currentWorkspace) + + setSelectedWorkspace(workspace) + }, [currentWorkspace]) + const renameCurrentWorkspace = () => { global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '') } @@ -51,6 +61,10 @@ export function Workspace () { global.modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '') } + const cloneGitRepository = () => { + global.modal('Clone Git Repository', cloneModalMessage(), 'OK', handleTypingUrl, '') + } + const downloadWorkspaces = async () => { try { await global.dispatchHandleDownloadFiles() @@ -124,6 +138,16 @@ export function Workspace () { workspaceCreateInput.current.value = `${workspaceCreateTemplateInput.current.value || 'remixDefault'}_${Date.now()}` } + const handleTypingUrl = () => { + const url = cloneUrlRef.current.value + + if (url) { + global.dispatchCloneRepository(url) + } else { + global.modal('Clone Git Repository', 'Please provide a valid git repository url.', 'OK', () => {}, '') + } + } + const createModalMessage = () => { return ( <> @@ -149,6 +173,14 @@ export function Workspace () { ) } + const cloneModalMessage = () => { + return ( + <> + + + ) + } + return (
@@ -158,111 +190,142 @@ export function Workspace () { - - - - - - + + + { selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? 'localhost' : NO_WORKSPACE } + + + + { + global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => ( + { + switchWorkspace(name) + }} + data-id={`dropdown-item-${name}`} + > + { isGitRepo ? +
+ { currentWorkspace === name ? ✓ { name } : { name } } + +
: + { currentWorkspace === name ? ✓ { name } : { name } } + } +
+ )) + } + { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? ✓ localhost : { LOCALHOST } } + { ((global.fs.browser.workspaces.length <= 0) || currentWorkspace === NO_WORKSPACE) && { switchWorkspace(NO_WORKSPACE) }}>{ NO_WORKSPACE } } +
+
-
- { (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && - - } -
+ { global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning ?
+ :
+ { (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && + + } +
+ } { global.fs.localhost.isRequestingLocalhost ?
:
diff --git a/nx.json b/nx.json index c6fca5f4ad..13cd26c2b2 100644 --- a/nx.json +++ b/nx.json @@ -169,6 +169,9 @@ }, "remix-ui-permission-handler": { "tags": [] + }, + "remix-ui-tooltip-popup": { + "tags": [] } }, "targetDependencies": { diff --git a/package.json b/package.json index cced59ff97..54a2b7c8aa 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,remix-ws-templates,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,remix-ui-run-tab,remix-ui-permission-handler,remix-ui-search", + "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,remix-ws-templates,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,remix-ui-run-tab,remix-ui-permission-handler,remix-ui-search,remix-ui-tooltip-popup", "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,remix-ws-templates,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": "yarn run build:libs && lerna publish --skip-git && yarn run bumpVersion:libs", diff --git a/tsconfig.base.json b/tsconfig.base.json index 409590c800..e97aaf9aff 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -87,7 +87,8 @@ "@remix-ui/run-tab": ["libs/remix-ui/run-tab/src/index.ts"], "@remix-ui/permission-handler": [ "libs/remix-ui/permission-handler/src/index.ts" - ] + ], + "@remix-ui/tooltip-popup": ["libs/remix-ui/tooltip-popup/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 7ead3dd32b..02a11c683b 100644 --- a/workspace.json +++ b/workspace.json @@ -1253,6 +1253,22 @@ } } } + }, + "remix-ui-tooltip-popup": { + "root": "libs/remix-ui/tooltip-popup", + "sourceRoot": "libs/remix-ui/tooltip-popup/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "config": "libs/remix-ui/tooltip-popup/.eslintrc.json", + "tsConfig": ["libs/remix-ui/tooltip-popup/tsconfig.lib.json"], + "exclude": ["**/node_modules/**"] + } + } + } } }, "cli": {