Merge pull request #2554 from ethereum/clone

Clone
pull/5370/head
David Disu 2 years ago committed by GitHub
commit 029e6dda90
  1. 14
      apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts
  2. 18
      apps/remix-ide-e2e/src/commands/switchWorkspace.ts
  3. 4
      apps/remix-ide-e2e/src/tests/generalSettings.test.ts
  4. 4
      apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts
  5. 8
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  6. 2
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  7. 4
      apps/remix-ide-e2e/src/tests/url.test.ts
  8. 14
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  9. 3
      apps/remix-ide-e2e/src/types/index.d.ts
  10. 7
      apps/remix-ide/src/app/files/dgitProvider.js
  11. 9
      apps/remix-ide/src/app/files/fileManager.ts
  12. 16
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  13. 81
      libs/remix-ui/settings/src/lib/github-settings.tsx
  14. 15
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
  15. 4
      libs/remix-ui/settings/src/lib/settingsAction.ts
  16. 12
      libs/remix-ui/settings/src/types/index.ts
  17. 12
      libs/remix-ui/tooltip-popup/.babelrc
  18. 18
      libs/remix-ui/tooltip-popup/.eslintrc.json
  19. 7
      libs/remix-ui/tooltip-popup/README.md
  20. 1
      libs/remix-ui/tooltip-popup/src/index.ts
  21. 0
      libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.module.css
  22. 27
      libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.tsx
  23. 6
      libs/remix-ui/tooltip-popup/src/types/index.ts
  24. 20
      libs/remix-ui/tooltip-popup/tsconfig.json
  25. 13
      libs/remix-ui/tooltip-popup/tsconfig.lib.json
  26. 21
      libs/remix-ui/workspace/src/lib/actions/index.ts
  27. 24
      libs/remix-ui/workspace/src/lib/actions/payload.ts
  28. 69
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  29. 42
      libs/remix-ui/workspace/src/lib/components/custom-dropdown.tsx
  30. 5
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  31. 36
      libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css
  32. 9
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  33. 73
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  34. 87
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  35. 3
      nx.json
  36. 2
      package.json
  37. 3
      tsconfig.base.json
  38. 16
      workspace.json

@ -3,13 +3,13 @@ import EventEmitter from 'events'
class CurrentWorkspaceIs extends EventEmitter { class CurrentWorkspaceIs extends EventEmitter {
command (this: NightwatchBrowser, name: string): NightwatchBrowser { command (this: NightwatchBrowser, name: string): NightwatchBrowser {
this.api const browser = this.api
.execute(function () {
const el = document.querySelector('select[data-id="workspacesSelect"]') as HTMLSelectElement browser.getText('[data-id="workspacesSelect"]', function (result) {
return el.value browser.assert.equal(result.value, name)
}, [], (result) => { })
console.log(result) .perform((done) => {
this.api.assert.equal(result.value, name) done()
this.emit('complete') this.emit('complete')
}) })
return this return this

@ -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

@ -41,7 +41,7 @@ module.exports = {
.setValue('*[data-id="settingsTabGistAccessToken"]', '**********') .setValue('*[data-id="settingsTabGistAccessToken"]', '**********')
.click('*[data-id="settingsTabSaveGistToken"]') .click('*[data-id="settingsTabSaveGistToken"]')
.waitForElementVisible('*[data-shared="tooltipPopup"]', 5000) .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) .pause(3000)
}, },
@ -59,7 +59,7 @@ module.exports = {
.pause(1000) .pause(1000)
.click('*[data-id="settingsTabRemoveGistToken"]') .click('*[data-id="settingsTabRemoveGistToken"]')
.waitForElementVisible('*[data-shared="tooltipPopup"]', 5000) .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"]', '') .assert.containsText('*[data-id="settingsTabGistAccessToken"]', '')
}, },

@ -81,7 +81,7 @@ module.exports = {
// these are test data entries // these are test data entries
'Should have a workspace_test #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { 'Should have a workspace_test #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000)
.click('*[data-id="workspacesSelect"] option[value="workspace_test"]') .switchWorkspace('workspace_test')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest_contracts"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest_contracts"]')
}, },
'Should have a sol file with test data #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { '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) { 'Should have a empty workspace #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000)
.click('*[data-id="workspacesSelect"] option[value="emptyspace"]') .switchWorkspace('emptyspace')
}, },
// end of test data entries // end of test data entries
'Should load with all storage blocked #group4': function (browser: NightwatchBrowser) { 'Should load with all storage blocked #group4': function (browser: NightwatchBrowser) {

@ -298,25 +298,25 @@ module.exports = {
}, null, null) }, null, null)
}, },
'Should get all workspaces #group2': async function (browser: NightwatchBrowser) { '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) { '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) { 'Should have event when switching workspace #group2': async function (browser: NightwatchBrowser) {
// @ts-ignore // @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) await clickAndCheckLog(browser, null, null, { event: 'setWorkspace', args: [{ name: 'default_workspace', isLocalhost: false }] }, null)
}) })
}, },
'Should rename workspace #group2': async function (browser: NightwatchBrowser) { 'Should rename workspace #group2': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:renameWorkspace', null, null, ['default_workspace', 'renamed']) 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) { 'Should delete workspace #group2': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:deleteWorkspace', null, null, ['testspace']) 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 // DGIT
'Should have changes on new workspace #group3': async function (browser: NightwatchBrowser) { 'Should have changes on new workspace #group3': async function (browser: NightwatchBrowser) {

@ -181,7 +181,7 @@ module.exports = {
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' })
.waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]') .waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]')
.execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() }) .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_tests.sol"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]') .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]')
.openFile('.deps/remix-tests/remix_tests.sol') .openFile('.deps/remix-tests/remix_tests.sol')

@ -29,6 +29,7 @@ module.exports = {
.click('[for="autoCompile"]') // we set it too false again .click('[for="autoCompile"]') // we set it too false again
.click('[for="autoCompile"]') // back to True in the local storage .click('[for="autoCompile"]') // back to True in the local storage
.assert.containsText('*[data-id="compilerContainerCompileBtn"]', 'contract-76747f6e19.sol') .assert.containsText('*[data-id="compilerContainerCompileBtn"]', 'contract-76747f6e19.sol')
.clickLaunchIcon('filePanel')
.currentWorkspaceIs('code-sample') .currentWorkspaceIs('code-sample')
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content && content.indexOf( 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') .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 .refresh() // we do one reload for making sure we already have the default workspace
.pause(5000) .pause(5000)
.clickLaunchIcon('filePanel')
.currentWorkspaceIs('code-sample') .currentWorkspaceIs('code-sample')
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content && content.indexOf( 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') .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() .refresh()
.pause(5000) .pause(5000)
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="code-sample"]') .switchWorkspace('code-sample')
.openFile('@openzeppelin') .openFile('@openzeppelin')
.openFile('@openzeppelin/contracts') .openFile('@openzeppelin/contracts')
.openFile('@openzeppelin/contracts/access') .openFile('@openzeppelin/contracts/access')

@ -235,7 +235,7 @@ module.exports = {
.pause(2000) .pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.pause(2000) .pause(2000)
.click('*[data-id="workspacesSelect"] option[value="workspace_name"]') .switchWorkspace('workspace_name')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
}, },
@ -249,23 +249,23 @@ module.exports = {
.setValue('*[data-id="modalDialogCustomPromptTextRename"]', 'workspace_name_renamed') .setValue('*[data-id="modalDialogCustomPromptTextRename"]', 'workspace_name_renamed')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .switchWorkspace('workspace_name_1')
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.pause(2000) .pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') .switchWorkspace('workspace_name_renamed')
.click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]')
.pause(2000) .pause(2000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
}, },
'Should delete a workspace #group1': function (browser: NightwatchBrowser) { 'Should delete a workspace #group1': function (browser: NightwatchBrowser) {
browser browser
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .switchWorkspace('workspace_name_1')
.click('*[data-id="workspaceDelete"]') // delete workspace_name_1 .click('*[data-id="workspaceDelete"]') // delete workspace_name_1
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[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() .end()
}, },

@ -62,7 +62,8 @@ declare module 'nightwatch' {
clearConsole (this: NightwatchBrowser): NightwatchBrowser clearConsole (this: NightwatchBrowser): NightwatchBrowser
clearTransactions (this: NightwatchBrowser): NightwatchBrowser clearTransactions (this: NightwatchBrowser): NightwatchBrowser
getBrowserLogs (this: NightwatchBrowser): NightwatchBrowser getBrowserLogs (this: NightwatchBrowser): NightwatchBrowser
currentSelectedFileIs (name: string): NightwatchBrowser currentSelectedFileIs (name: string): NightwatchBrowser,
switchWorkspace: (workspaceName: string) => NightwatchBrowser
} }
export interface NightwatchBrowser { export interface NightwatchBrowser {

@ -233,12 +233,11 @@ class DGitProvider extends Plugin {
return this.calculateLocalStorage() return this.calculateLocalStorage()
} }
async clone (input) { async clone (input, workspaceName, workspaceExists = false) {
const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.') const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.')
if (!permission) return false if (!permission) return false
if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.') 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 = { const cmd = {
url: input.url, url: input.url,
singleBranch: input.singleBranch, singleBranch: input.singleBranch,
@ -249,9 +248,11 @@ class DGitProvider extends Plugin {
} }
const result = await git.clone(cmd) const result = await git.clone(cmd)
if (!workspaceExists) {
setTimeout(async () => { setTimeout(async () => {
await this.call('fileManager', 'refresh') await this.call('fileManager', 'refresh')
}, 1000) }, 1000)
}
return result return result
} }

@ -19,7 +19,7 @@ const profile = {
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
permission: true, permission: true,
version: packageJson.version, version: packageJson.version,
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', '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' kind: 'file-system'
} }
const errorMsg = { const errorMsg = {
@ -810,6 +810,13 @@ class FileManager extends Plugin {
return provider.workspace return provider.workspace
} }
} }
async isGitRepo (directory: string): Promise<boolean> {
const path = directory + '/.git'
const exists = await this.exists(path)
return exists
}
} }
module.exports = FileManager module.exports = FileManager

@ -47,6 +47,22 @@ export const createNonClashingNameAsync = async (name: string, fileManager, pref
return name + counter + prefix + '.' + ext 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) => { export const joinPath = (...paths) => {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
if (paths.length === 1) return paths[0] if (paths.length === 1) return paths[0]

@ -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<string>("")
const [githubUserName, setGithubUsername] = useState<string>("")
const [githubEmail, setGithubEmail] = useState<string>("")
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 (
<div className="border-top">
<div className="card-body pt-3 pb-2">
<h6 className="card-title">GitHub Credentials</h6>
<p className="mb-1">Manage your GitHub credentials used to publish to Gist and retrieve GitHub contents.</p>
<p className="">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.</p>
<p className="mb-1"><a className="text-primary" target="_blank" href="https://github.com/settings/tokens">https://github.com/settings/tokens</a></p>
<div>
<label>TOKEN:</label>
<div className="input-group text-secondary mb-0 h6">
<input id="gistaccesstoken" data-id="settingsTabGistAccessToken" type="password" className="form-control" onChange={(e) => handleChangeTokenState(e)} value={ githubToken } />
<div className="input-group-append">
<CopyToClipboard content={githubToken} data-id='copyToClipboardCopyIcon' className='far fa-copy ml-1 p-2 mt-1' direction={"top"} />
</div>
</div>
</div>
<div>
<label>USERNAME:</label>
<div className="text-secondary mb-0 h6">
<input id="githubusername" data-id="settingsTabGithubUsername" type="text" className="form-control" onChange={(e) => handleChangeUserNameState(e)} value={ githubUserName } />
</div>
</div>
<div>
<label>EMAIL:</label>
<div className="text-secondary mb-0 h6">
<input id="githubemail" data-id="settingsTabGithubEmail" type="text" className="form-control" onChange={(e) => handleChangeEmailState(e)} value={ githubEmail } />
<div className="d-flex justify-content-end pt-2">
<input className="btn btn-sm btn-primary ml-2" id="savegisttoken" data-id="settingsTabSaveGistToken" onClick={saveGithubToken} value="Save" type="button" disabled={githubToken === ''}></input>
<button className="btn btn-sm btn-secondary ml-2" id="removegisttoken" data-id="settingsTabRemoveGistToken" title="Delete GitHub Credentials" onClick={removeToken}>Remove</button>
</div>
</div>
</div>
</div>
</div>
)
}

@ -8,6 +8,7 @@ import { ethereumVM, generateContractMetadat, personal, textWrapEventAction, use
import { initialState, toastInitialState, toastReducer, settingReducer } from './settingsReducer' import { initialState, toastInitialState, toastReducer, settingReducer } from './settingsReducer'
import { Toaster } from '@remix-ui/toaster'// eslint-disable-line import { Toaster } from '@remix-ui/toaster'// eslint-disable-line
import { RemixUiThemeModule, ThemeModule} from '@remix-ui/theme-module' import { RemixUiThemeModule, ThemeModule} from '@remix-ui/theme-module'
import { GithubSettings } from './github-settings'
/* eslint-disable-next-line */ /* eslint-disable-next-line */
export interface RemixUiSettingsProps { export interface RemixUiSettingsProps {
@ -347,7 +348,19 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
<div> <div>
{state.message ? <Toaster message= {state.message}/> : null} {state.message ? <Toaster message= {state.message}/> : null}
{generalConfig()} {generalConfig()}
{token('gist')} <GithubSettings
saveTokenToast={(githubToken: string, githubUserName: string, githubEmail: string) => {
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')} {token('etherscan')}
{swarmSettings()} {swarmSettings()}
{ipfsSettings()} {ipfsSettings()}

@ -43,12 +43,12 @@ export const useMatomoAnalytics = (config, checked, dispatch) => {
export const saveTokenToast = (config, dispatch, tokenValue, key) => { export const saveTokenToast = (config, dispatch, tokenValue, key) => {
config.set('settings/' + key, tokenValue) 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) => { export const removeTokenToast = (config, dispatch, key) => {
config.set('settings/' + 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) => { export const saveSwarmSettingsToast = (config, dispatch, privateBeeAddress, postageStampId) => {

@ -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
}
}

@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

@ -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": {}
}
]
}

@ -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).

@ -0,0 +1 @@
export * from './lib/tooltip-popup'

@ -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) => (
<Popover id="popover-basic" className='bg-light border-secondary'>
<Popover.Title as="h3" className='bg-dark border-0'>{ title || 'Tooltip' }</Popover.Title>
<Popover.Content>
{ content }
</Popover.Content>
</Popover>
)
export function TooltipPopup(props: TooltipPopupProps) {
const [show, setShow] = useState<boolean>(false)
return (
<OverlayTrigger trigger="click" placement={"bottom"} overlay={popover(props.title, props.children || props.content)} show={show} onToggle={(nextShow) => {
setShow(nextShow)
}}>
<i className={`${props.icon} remixui_menuicon pr-0 mr-2`}></i>
</OverlayTrigger>
)
}
export default TooltipPopup

@ -0,0 +1,6 @@
export interface TooltipPopupProps {
children?: React.ReactNode,
title?: string,
content?: string,
icon: string
}

@ -0,0 +1,20 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -24,17 +24,20 @@ export type UrlParametersType = {
url: string url: string
} }
const basicWorkspaceInit = async (workspaces, workspaceProvider) => { const basicWorkspaceInit = async (workspaces: { name: string; isGitRepo: boolean; }[], workspaceProvider) => {
if (workspaces.length === 0) { if (workspaces.length === 0) {
await createWorkspaceTemplate('default_workspace', 'remixDefault') await createWorkspaceTemplate('default_workspace', 'remixDefault')
plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false }) plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false })
dispatch(setCurrentWorkspace('default_workspace')) dispatch(setCurrentWorkspace({ name: 'default_workspace', isGitRepo: false }))
await loadWorkspacePreset('remixDefault') await loadWorkspacePreset('remixDefault')
} else { } else {
if (workspaces.length > 0) { if (workspaces.length > 0) {
workspaceProvider.setWorkspace(workspaces[workspaces.length - 1]) const workspace = workspaces[workspaces.length - 1]
plugin.setWorkspace({ name: workspaces[workspaces.length - 1], isLocalhost: false }) const workspaceName = (workspace || {}).name
dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1]))
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) { if (params.gist) {
await createWorkspaceTemplate('gist-sample', 'gist-template') await createWorkspaceTemplate('gist-sample', 'gist-template')
plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false }) plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('gist-sample')) dispatch(setCurrentWorkspace({ name: 'gist-sample', isGitRepo: false }))
await loadWorkspacePreset('gist-template') await loadWorkspacePreset('gist-template')
} else if (params.code || params.url) { } else if (params.code || params.url) {
await createWorkspaceTemplate('code-sample', 'code-template') await createWorkspaceTemplate('code-sample', 'code-template')
plugin.setWorkspace({ name: 'code-sample', isLocalhost: false }) plugin.setWorkspace({ name: 'code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('code-sample')) dispatch(setCurrentWorkspace({ name: 'code-sample', isGitRepo: false }))
const filePath = await loadWorkspacePreset('code-template') const filePath = await loadWorkspacePreset('code-template')
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath)) plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath))
} else if (window.location.pathname && window.location.pathname !== '/') { } else if (window.location.pathname && window.location.pathname !== '/') {
@ -95,7 +98,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
foundOnNetworks.push(network.name) foundOnNetworks.push(network.name)
await createWorkspaceTemplate('etherscan-code-sample', 'code-template') await createWorkspaceTemplate('etherscan-code-sample', 'code-template')
plugin.setWorkspace({ name: 'etherscan-code-sample', isLocalhost: false }) plugin.setWorkspace({ name: 'etherscan-code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('etherscan-code-sample')) dispatch(setCurrentWorkspace({ name: 'etherscan-code-sample', isGitRepo: false }))
let filePath let filePath
count = count + (Object.keys(data.compilationTargets)).length count = count + (Object.keys(data.compilationTargets)).length
for (filePath in data.compilationTargets) for (filePath in data.compilationTargets)
@ -119,7 +122,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
const content = response.data const content = response.data
await createWorkspaceTemplate('github-code-sample', 'code-template') await createWorkspaceTemplate('github-code-sample', 'code-template')
plugin.setWorkspace({ name: 'github-code-sample', isLocalhost: false }) 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) await workspaceProvider.set(route, content)
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(route)) plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(route))
} else await basicWorkspaceInit(workspaces, workspaceProvider) } else await basicWorkspaceInit(workspaces, workspaceProvider)

@ -1,13 +1,13 @@
import { action } from '../types' import { action } from '../types'
export const setCurrentWorkspace = (workspace: string) => { export const setCurrentWorkspace = (workspace: { name: string; isGitRepo: boolean; }) => {
return { return {
type: 'SET_CURRENT_WORKSPACE', type: 'SET_CURRENT_WORKSPACE',
payload: workspace payload: workspace
} }
} }
export const setWorkspaces = (workspaces: string[]) => { export const setWorkspaces = (workspaces: { name: string; isGitRepo: boolean; }[]) => {
return { return {
type: 'SET_WORKSPACES', type: 'SET_WORKSPACES',
payload: workspaces payload: workspaces
@ -125,7 +125,7 @@ export const createWorkspaceRequest = (promise: Promise<any>) => {
} }
} }
export const createWorkspaceSuccess = (workspaceName: string) => { export const createWorkspaceSuccess = (workspaceName: { name: string; isGitRepo: boolean; }) => {
return { return {
type: 'CREATE_WORKSPACE_SUCCESS', type: 'CREATE_WORKSPACE_SUCCESS',
payload: workspaceName payload: workspaceName
@ -239,3 +239,21 @@ export const fsInitializationCompleted = () => {
type: 'FS_INITIALIZATION_COMPLETED' 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'
}
}

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import { bufferToHex, keccakFromString } from 'ethereumjs-util' import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload' import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload'
import { checkSlash, checkSpecialChars } from '@remix-ui/helper' import { checkSlash, checkSpecialChars, createNonClashingTitle } from '@remix-ui/helper'
import { JSONStandardInput, WorkspaceTemplate } from '../types' import { JSONStandardInput, WorkspaceTemplate } from '../types'
import { QueryParams } from '@remix-project/remix-lib' import { QueryParams } from '@remix-project/remix-lib'
@ -42,13 +42,13 @@ export const addInputField = async (type: 'file' | 'folder', path: string, cb?:
return promise return promise
} }
export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void, isGitRepo: boolean = false) => {
await plugin.fileManager.closeAllFiles() await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName) const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName)
dispatch(createWorkspaceRequest(promise)) dispatch(createWorkspaceRequest(promise))
promise.then(async () => { promise.then(async () => {
dispatch(createWorkspaceSuccess(workspaceName)) dispatch(createWorkspaceSuccess({ name: workspaceName, isGitRepo }))
await plugin.setWorkspace({ name: workspaceName, isLocalhost: false }) await plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
await plugin.setWorkspaces(await getWorkspaces()) await plugin.setWorkspaces(await getWorkspaces())
await plugin.workspaceCreated(workspaceName) await plugin.workspaceCreated(workspaceName)
@ -254,8 +254,10 @@ export const switchToWorkspace = async (name: string) => {
if (isActive) await plugin.call('manager', 'deactivatePlugin', 'remixd') if (isActive) await plugin.call('manager', 'deactivatePlugin', 'remixd')
await plugin.fileProviders.workspace.setWorkspace(name) await plugin.fileProviders.workspace.setWorkspace(name)
await plugin.setWorkspace({ name, isLocalhost: false }) await plugin.setWorkspace({ name, isLocalhost: false })
const isGitRepo = await plugin.fileManager.isGitRepo()
dispatch(setMode('browser')) dispatch(setMode('browser'))
dispatch(setCurrentWorkspace(name)) dispatch(setCurrentWorkspace({ name, isGitRepo }))
dispatch(setReadOnlyMode(false)) dispatch(setReadOnlyMode(false))
} }
} }
@ -302,22 +304,69 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
}) })
} }
export const getWorkspaces = async (): Promise<string[]> | undefined => { export const getWorkspaces = async (): Promise<{name: string, isGitRepo: boolean}[]> | undefined => {
try { 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 const workspacesPath = plugin.fileProviders.workspace.workspacesPath
plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => { plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) { if (error) {
return reject(error) return reject(error)
} }
resolve(Object.keys(items) Promise.all(Object.keys(items)
.filter((item) => items[item].isDirectory) .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) await plugin.setWorkspaces(workspaces)
return 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))
}
}

@ -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<HTMLButtonElement>) => (
<button
ref={ref}
onClick={(e) => {
e.preventDefault()
onClick(e)
}}
className={className.replace('dropdown-toggle', '')}
>
<div className="d-flex">
<div className="mr-auto">{ children }</div>
{ icon && <div className="pr-1"><i className={`${icon} pr-1`}></i></div> }
<div><i className="fad fa-sort-circle"></i></div>
</div>
</button>
))
// 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<HTMLDivElement>) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<ul className="list-unstyled mb-0">
{
children
}
</ul>
</div>
)
},
)

@ -4,7 +4,7 @@ import { BrowserState } from '../reducers/workspace'
export const FileSystemContext = createContext<{ export const FileSystemContext = createContext<{
fs: BrowserState, 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<void>, dispatchInitWorkspace:() => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>, dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>, dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
@ -29,5 +29,6 @@ export const FileSystemContext = createContext<{
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void> dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>, dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchHandleDownloadFiles: () => Promise<void>, dispatchHandleDownloadFiles: () => Promise<void>,
dispatchHandleRestoreBackup: () => Promise<void> dispatchHandleRestoreBackup: () => Promise<void>,
dispatchCloneRepository: (url: string) => Promise<void>
}>(null) }>(null)

@ -62,4 +62,40 @@
.remixui_menuicon:hover { .remixui_menuicon:hover {
transform: scale(1.3); 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;
}

@ -5,7 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileSystemContext } from '../contexts' import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace' 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' import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace' import { Workspace } from '../remix-ui-workspace'
@ -123,6 +123,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await restoreBackupZip() await restoreBackupZip()
} }
const dispatchCloneRepository = async (url: string) => {
await cloneRepository(url)
}
useEffect(() => { useEffect(() => {
dispatchInitWorkspace() dispatchInitWorkspace()
}, []) }, [])
@ -224,7 +228,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchHandleClickFile, dispatchHandleClickFile,
dispatchHandleExpandPath, dispatchHandleExpandPath,
dispatchHandleDownloadFiles, dispatchHandleDownloadFiles,
dispatchHandleRestoreBackup dispatchHandleRestoreBackup,
dispatchCloneRepository
} }
return ( return (
<FileSystemContext.Provider value={value}> <FileSystemContext.Provider value={value}>

@ -8,13 +8,18 @@ interface Action {
export interface BrowserState { export interface BrowserState {
browser: { browser: {
currentWorkspace: string, currentWorkspace: string,
workspaces: string[], workspaces: {
name: string;
isGitRepo: boolean;
}[],
files: { [x: string]: Record<string, FileType> }, files: { [x: string]: Record<string, FileType> },
expandPath: string[] expandPath: string[]
isRequestingDirectory: boolean, isRequestingDirectory: boolean,
isSuccessfulDirectory: boolean, isSuccessfulDirectory: boolean,
isRequestingWorkspace: boolean, isRequestingWorkspace: boolean,
isSuccessfulWorkspace: boolean, isSuccessfulWorkspace: boolean,
isRequestingCloning: boolean,
isSuccessfulCloning: boolean,
error: string, error: string,
contextMenu: { contextMenu: {
registeredMenuItems: action[], registeredMenuItems: action[],
@ -63,6 +68,8 @@ export const browserInitialState: BrowserState = {
isSuccessfulDirectory: false, isSuccessfulDirectory: false,
isRequestingWorkspace: false, isRequestingWorkspace: false,
isSuccessfulWorkspace: false, isSuccessfulWorkspace: false,
isRequestingCloning: false,
isSuccessfulCloning: false,
error: null, error: null,
contextMenu: { contextMenu: {
registeredMenuItems: [], registeredMenuItems: [],
@ -104,21 +111,21 @@ export const browserInitialState: BrowserState = {
export const browserReducer = (state = browserInitialState, action: Action) => { export const browserReducer = (state = browserInitialState, action: Action) => {
switch (action.type) { switch (action.type) {
case 'SET_CURRENT_WORKSPACE': { case 'SET_CURRENT_WORKSPACE': {
const payload = action.payload as string const payload = action.payload as { name: string; isGitRepo: boolean; }
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return { return {
...state, ...state,
browser: { browser: {
...state.browser, ...state.browser,
currentWorkspace: payload, currentWorkspace: payload.name,
workspaces: workspaces.filter(workspace => workspace) workspaces: workspaces.filter(workspace => workspace)
} }
} }
} }
case 'SET_WORKSPACES': { case 'SET_WORKSPACES': {
const payload = action.payload as string[] const payload = action.payload as { name: string; isGitRepo: boolean; }[]
return { return {
...state, ...state,
@ -416,14 +423,14 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
} }
case 'CREATE_WORKSPACE_SUCCESS': { case 'CREATE_WORKSPACE_SUCCESS': {
const payload = action.payload as string const payload = action.payload as { name: string; isGitRepo: boolean; }
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return { return {
...state, ...state,
browser: { browser: {
...state.browser, ...state.browser,
currentWorkspace: payload, currentWorkspace: payload.name,
workspaces: workspaces.filter(workspace => workspace), workspaces: workspaces.filter(workspace => workspace),
isRequestingWorkspace: false, isRequestingWorkspace: false,
isSuccessfulWorkspace: true, isSuccessfulWorkspace: true,
@ -446,14 +453,25 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
case 'RENAME_WORKSPACE': { case 'RENAME_WORKSPACE': {
const payload = action.payload as { oldName: string, workspaceName: string } 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 { return {
...state, ...state,
browser: { browser: {
...state.browser, ...state.browser,
currentWorkspace: payload.workspaceName, currentWorkspace: payload.workspaceName,
workspaces: [...workspaces, payload.workspaceName], workspaces: [...workspaces, renamedWorkspace],
expandPath: [] expandPath: []
} }
} }
@ -461,7 +479,7 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
case 'DELETE_WORKSPACE': { case 'DELETE_WORKSPACE': {
const payload = action.payload as string 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 { return {
...state, ...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': { case 'FS_INITIALIZATION_COMPLETED': {
return { return {
...state, ...state,

@ -1,7 +1,9 @@
import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line 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 { FileExplorer } from './components/file-explorer' // eslint-disable-line
import './css/remix-ui-workspace.css'
import { FileSystemContext } from './contexts' import { FileSystemContext } from './contexts'
import './css/remix-ui-workspace.css'
const canUpload = window.File || window.FileReader || window.FileList || window.Blob const canUpload = window.File || window.FileReader || window.FileList || window.Blob
@ -9,10 +11,12 @@ export function Workspace () {
const LOCALHOST = ' - connect to localhost - ' const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - ' const NO_WORKSPACE = ' - none - '
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE) const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const [selectedWorkspace, setSelectedWorkspace] = useState<{ name: string, isGitRepo: boolean}>(null)
const global = useContext(FileSystemContext) const global = useContext(FileSystemContext)
const workspaceRenameInput = useRef() const workspaceRenameInput = useRef()
const workspaceCreateInput = useRef() const workspaceCreateInput = useRef()
const workspaceCreateTemplateInput = useRef() const workspaceCreateTemplateInput = useRef()
const cloneUrlRef = useRef<HTMLInputElement>()
useEffect(() => { useEffect(() => {
resetFocus() resetFocus()
@ -30,15 +34,21 @@ export function Workspace () {
}, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode]) }, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode])
useEffect(() => { 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) { 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 { } else {
switchWorkspace(NO_WORKSPACE) switchWorkspace(NO_WORKSPACE)
} }
} }
}, [global.fs.browser.workspaces]) }, [global.fs.browser.workspaces])
useEffect(() => {
const workspace = global.fs.browser.workspaces.find(workspace => workspace.name === currentWorkspace)
setSelectedWorkspace(workspace)
}, [currentWorkspace])
const renameCurrentWorkspace = () => { const renameCurrentWorkspace = () => {
global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '') 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, '') 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 () => { const downloadWorkspaces = async () => {
try { try {
await global.dispatchHandleDownloadFiles() await global.dispatchHandleDownloadFiles()
@ -124,6 +138,16 @@ export function Workspace () {
workspaceCreateInput.current.value = `${workspaceCreateTemplateInput.current.value || 'remixDefault'}_${Date.now()}` 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 = () => { const createModalMessage = () => {
return ( return (
<> <>
@ -149,6 +173,14 @@ export function Workspace () {
) )
} }
const cloneModalMessage = () => {
return (
<>
<input type="text" data-id="modalDialogCustomPromptTextClone" placeholder='Enter git repository url' ref={cloneUrlRef} className="form-control" />
</>
)
}
return ( return (
<div className='remixui_container'> <div className='remixui_container'>
<div className='remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}> <div className='remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
@ -214,23 +246,53 @@ export function Workspace () {
className='far fa-upload remixui_menuicon' className='far fa-upload remixui_menuicon'
title='Restore Workspaces Backup'> title='Restore Workspaces Backup'>
</span> </span>
<span
id='cloneGitRepository'
data-id='cloneGitRepository'
onClick={(e) => {
e.stopPropagation()
cloneGitRepository()
}}
className='far fa-clone remixui_menuicon'
title='Clone Git Repository'>
</span>
</span> </span>
<select id="workspacesSelect" value={currentWorkspace} data-id="workspacesSelect" onChange={(e) => switchWorkspace(e.target.value)} className="form-control custom-select"> <Dropdown id="workspacesSelect" data-id="workspacesSelect">
<Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control" icon={selectedWorkspace && selectedWorkspace.isGitRepo ? 'far fa-code-branch' : null}>
{ selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? 'localhost' : NO_WORKSPACE }
</Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='w-100 custom-dropdown-items' data-id="custom-dropdown-items" >
{ {
global.fs.browser.workspaces global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => (
.map((folder, index) => { <Dropdown.Item
return <option key={index} value={folder}>{folder}</option> key={index}
}) onClick={() => {
switchWorkspace(name)
}}
data-id={`dropdown-item-${name}`}
>
{ isGitRepo ?
<div className='d-flex justify-content-between'>
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
<i className='fas fa-code-branch pt-1'></i>
</div> :
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
} }
<option value={LOCALHOST}>{currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option> </Dropdown.Item>
{ global.fs.browser.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> } ))
</select> }
<Dropdown.Item onClick={() => { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? <span>&#10003; localhost </span> : <span className="pl-3"> { LOCALHOST } </span>}</Dropdown.Item>
{ ((global.fs.browser.workspaces.length <= 0) || currentWorkspace === NO_WORKSPACE) && <Dropdown.Item onClick={() => { switchWorkspace(NO_WORKSPACE) }}>{ <span className="pl-3">NO_WORKSPACE</span> }</Dropdown.Item> }
</Dropdown.Menu>
</Dropdown>
</div> </div>
</header> </header>
</div> </div>
<div className='h-100 remixui_fileExplorerTree'> <div className='h-100 remixui_fileExplorerTree'>
<div className='h-100'> <div className='h-100'>
<div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'> { global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>
: <div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'>
{ (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && { (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<FileExplorer <FileExplorer
name={currentWorkspace} name={currentWorkspace}
@ -263,6 +325,7 @@ export function Workspace () {
/> />
} }
</div> </div>
}
{ {
global.fs.localhost.isRequestingLocalhost ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> global.fs.localhost.isRequestingLocalhost ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>
: <div className='pl-2 filesystemexplorer remixui_treeview'> : <div className='pl-2 filesystemexplorer remixui_treeview'>

@ -169,6 +169,9 @@
}, },
"remix-ui-permission-handler": { "remix-ui-permission-handler": {
"tags": [] "tags": []
},
"remix-ui-tooltip-popup": {
"tags": []
} }
}, },
"targetDependencies": { "targetDependencies": {

@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic", "workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph", "dep-graph": "nx dep-graph",
"help": "nx help", "help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,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", "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", "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", "publish:libs": "yarn run build:libs && lerna publish --skip-git && yarn run bumpVersion:libs",

@ -87,7 +87,8 @@
"@remix-ui/run-tab": ["libs/remix-ui/run-tab/src/index.ts"], "@remix-ui/run-tab": ["libs/remix-ui/run-tab/src/index.ts"],
"@remix-ui/permission-handler": [ "@remix-ui/permission-handler": [
"libs/remix-ui/permission-handler/src/index.ts" "libs/remix-ui/permission-handler/src/index.ts"
] ],
"@remix-ui/tooltip-popup": ["libs/remix-ui/tooltip-popup/src/index.ts"]
} }
}, },
"exclude": ["node_modules", "tmp"] "exclude": ["node_modules", "tmp"]

@ -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": { "cli": {

Loading…
Cancel
Save