pull/3668/head
filip mertens 2 years ago
commit a4e58ed962
  1. 22
      .circleci/config.yml
  2. 3
      .github/workflows/publish-action.yml
  3. 2
      .github/workflows/run-sut.yml
  4. 2
      README.md
  5. 2
      apps/etherscan/src/app/views/VerifyView.tsx
  6. 2
      apps/remix-ide-e2e/package.json
  7. 1
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  8. 2
      apps/remix-ide-e2e/src/tests/workspace_git.test.ts
  9. 26
      apps/remix-ide/ci/download_e2e_assets.js
  10. 2
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  11. 4
      apps/remix-ide/src/app/tabs/locales/en/settings.json
  12. 19
      apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css
  13. 28
      apps/remix-ide/webpack.config.js
  14. 4
      libs/remix-debug/test/debugger.ts
  15. 2
      libs/remix-debug/test/sourceLocationTracker.ts
  16. 2
      libs/remix-lib/package.json
  17. 1
      libs/remix-tests/package.json
  18. 3
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/dropdown-panel.tsx
  19. 2
      libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx
  20. 12
      libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
  21. 2
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  22. 41
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  23. 2
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  24. 264
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  25. 31
      libs/remix-ui/workspace/src/lib/components/upload-file.tsx
  26. 313
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  27. 46
      libs/remix-ui/workspace/src/lib/types/index.ts
  28. 27
      libs/remix-ui/workspace/src/lib/utils/index.ts
  29. 11
      package.json
  30. 661
      yarn.lock

@ -9,7 +9,7 @@ orbs:
jobs: jobs:
build: build:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge
@ -43,7 +43,7 @@ jobs:
build-plugin: build-plugin:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge
working_directory: ~/remix-project working_directory: ~/remix-project
@ -69,7 +69,7 @@ jobs:
lint: lint:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge
working_directory: ~/remix-project working_directory: ~/remix-project
@ -86,7 +86,7 @@ jobs:
command: node ./apps/remix-ide/ci/lint-targets.js command: node ./apps/remix-ide/ci/lint-targets.js
remix-libs: remix-libs:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge
working_directory: ~/remix-project working_directory: ~/remix-project
@ -98,6 +98,7 @@ jobs:
- restore_cache: - restore_cache:
keys: keys:
- v1-deps-{{ checksum "yarn.lock" }} - v1-deps-{{ checksum "yarn.lock" }}
- run: yarn --version
- run: yarn - run: yarn
- run: yarn build:libs - run: yarn build:libs
- run: cd dist/libs/remix-tests && yarn - run: cd dist/libs/remix-tests && yarn
@ -111,7 +112,7 @@ jobs:
remix-ide-browser: remix-ide-browser:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge
working_directory: ~/remix-project working_directory: ~/remix-project
@ -154,7 +155,7 @@ jobs:
at: . at: .
- run: unzip ./persist/dist.zip - run: unzip ./persist/dist.zip
- run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules - run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules
- run: yarn run downloadsolc_assets_e2e && yarn run downloadsolc_assets_dist - run: yarn run downloadsolc_assets_e2e
- run: ls -la ./dist/apps/remix-ide/assets/js - run: ls -la ./dist/apps/remix-ide/assets/js
- run: yarn run selenium-install || yarn run selenium-install - run: yarn run selenium-install || yarn run selenium-install
- run: - run:
@ -175,7 +176,7 @@ jobs:
remix-test-plugins: remix-test-plugins:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge
working_directory: ~/remix-project working_directory: ~/remix-project
@ -201,7 +202,7 @@ jobs:
- run: unzip ./persist/dist.zip - run: unzip ./persist/dist.zip
- run: unzip ./persist/plugin-<< parameters.plugin >>.zip - run: unzip ./persist/plugin-<< parameters.plugin >>.zip
- run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules - run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules
- run: yarn run downloadsolc_assets_e2e && yarn run downloadsolc_assets_dist - run: yarn run downloadsolc_assets_e2e
- run: yarn run selenium-install || yarn run selenium-install - run: yarn run selenium-install || yarn run selenium-install
- run: - run:
name: Start Selenium name: Start Selenium
@ -216,7 +217,7 @@ jobs:
predeploy: predeploy:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge
working_directory: ~/remix-project working_directory: ~/remix-project
@ -231,7 +232,6 @@ jobs:
paths: paths:
- node_modules - node_modules
- run: yarn build:production - run: yarn build:production
- run: yarn run downloadsolc_assets_dist
- run: mkdir persist && zip -0 -r persist/predeploy.zip dist - run: mkdir persist && zip -0 -r persist/predeploy.zip dist
- persist_to_workspace: - persist_to_workspace:
root: . root: .
@ -240,7 +240,7 @@ jobs:
deploy-build: deploy-build:
docker: docker:
- image: cimg/node:14.17.6-browsers - image: cimg/node:20.0.0-browsers
resource_class: resource_class:
xlarge xlarge

@ -11,11 +11,10 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 14.17.6 node-version: 20.0.0
- run: yarn install - run: yarn install
- run: ls - run: ls
- run: pwd - run: pwd
- run: yarn run downloadsolc_assets
- run: yarn run build:production - run: yarn run build:production
- run: echo "action_state=$('./apps/remix-ide/ci/publishIpfs' ${{ secrets.IPFS_PROJET_ID }} ${{ secrets.IPFS_PROJECT_SECRET }})" >> $GITHUB_ENV - run: echo "action_state=$('./apps/remix-ide/ci/publishIpfs' ${{ secrets.IPFS_PROJET_ID }} ${{ secrets.IPFS_PROJECT_SECRET }})" >> $GITHUB_ENV
- uses: mshick/add-pr-comment@v1 - uses: mshick/add-pr-comment@v1

@ -12,7 +12,7 @@ jobs:
- name: Environment Setup - name: Environment Setup
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14.17.6 node-version: 20.0.0
- name: Run SUT Action - name: Run SUT Action
uses: EthereumRemix/sol-test@v1 uses: EthereumRemix/sol-test@v1
with: with:

@ -50,7 +50,7 @@ Note: It contains the latest supported version of Solidity available at the time
*Supported versions:* *Supported versions:*
```bash ```bash
"engines": { "engines": {
"node": "^14.17.6", "node": "^20.0.0",
"npm": "^6.14.15" "npm": "^6.14.15"
} }
``` ```

@ -125,7 +125,7 @@ export const VerifyView: React.FC<Props> = ({
}} }}
> >
<option disabled={true} value=""> <option disabled={true} value="">
Select a contract { contracts.length ? 'Select a contract' : `--- No compiled contracts ---` }
</option> </option>
{contracts.map((item) => ( {contracts.map((item) => (
<option key={item} value={item}> <option key={item} value={item}>

@ -2,7 +2,7 @@
"name": "remix-ide-e2e", "name": "remix-ide-e2e",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^14.17.6", "node": "^20.0.0",
"npm": "^6.14.15" "npm": "^6.14.15"
}, },
"dependencies": { "dependencies": {

@ -118,6 +118,7 @@ module.exports = {
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(100) .pause(100)
.waitForElementPresent('*[data-id="treeViewUltreeViewMenu"]') .waitForElementPresent('*[data-id="treeViewUltreeViewMenu"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.prettierrc.json"]')
.execute(function () { .execute(function () {
const fileList = document.querySelector('*[data-id="treeViewUltreeViewMenu"]') const fileList = document.querySelector('*[data-id="treeViewUltreeViewMenu"]')
return fileList.getElementsByTagName('li').length; return fileList.getElementsByTagName('li').length;

@ -15,7 +15,7 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button') .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button')
.waitForElementVisible({ .waitForElementVisible({
selector: "//*[@class='text-warning' and contains(.,'Please add username and email')]", selector: "//*[@class='text-warning' and contains(.,'add username and email')]",
locateStrategy: 'xpath' locateStrategy: 'xpath'
}) })
.waitForElementPresent({ .waitForElementPresent({

@ -20,14 +20,20 @@ axios({
const path = `./dist/apps/remix-ide/assets/js/${build.path}`; const path = `./dist/apps/remix-ide/assets/js/${build.path}`;
// use axios to get the file // use axios to get the file
try {
axios({ axios({
method: 'get', method: 'get',
url: buildurl, url: buildurl,
responseType: 'stream'
}).then(function (response) { }).then(function (response) {
// pipe the result stream into a file on disc fs.writeFile(path, response.data, function (err) {
response.data.pipe(fs.createWriteStream(path)); if (err) {
console.log(err);
}
})
}) })
} catch (e) {
console.log('Failed to download ' + build.path + ' from ' + buildurl)
}
} }
} }
@ -47,14 +53,22 @@ fs.readdirSync(testFolder).forEach(file => {
const path = `./dist/apps/remix-ide/assets/js/soljson${version}.js`; const path = `./dist/apps/remix-ide/assets/js/soljson${version}.js`;
// use axios to get the file // use axios to get the file
try {
axios({ axios({
method: 'get', method: 'get',
url: url, url: url,
responseType: 'stream'
}).then(function (response) { }).then(function (response) {
// pipe the result stream into a file on disc fs.writeFile(path, response.data, function (err) {
response.data.pipe(fs.createWriteStream(path)); if (err) {
console.log(err);
}
}) })
})
} catch (e) {
console.log('Failed to download soljson' + version + ' from ' + url)
}
} }
} }

@ -54,7 +54,7 @@
"filePanel.checkoutGitBranch": "Checkout Git Branch", "filePanel.checkoutGitBranch": "Checkout Git Branch",
"filePanel.findOrCreateABranch": "Find or create a branch.", "filePanel.findOrCreateABranch": "Find or create a branch.",
"filePanel.initGitRepositoryLabel": "Initialize workspace as a new git repository", "filePanel.initGitRepositoryLabel": "Initialize workspace as a new git repository",
"filePanel.initGitRepositoryWarning": "Please add username and email to Remix GitHub Settings to use git features.", "filePanel.initGitRepositoryWarning": "To use Git features, add username and email to the Github section of the Settings panel.",
"filePanel.workspaceName": "Workspace name", "filePanel.workspaceName": "Workspace name",
"filePanel.customizeTemplate": "Customize template", "filePanel.customizeTemplate": "Customize template",
"filePanel.features": "Features", "filePanel.features": "Features",

@ -11,8 +11,8 @@
"settings.matomoAnalytics": "Enable Matomo Analytics. We do not collect personally identifiable information (PII). The info is used to improve the site’s UX & UI. See more about ", "settings.matomoAnalytics": "Enable Matomo Analytics. We do not collect personally identifiable information (PII). The info is used to improve the site’s UX & UI. See more about ",
"settings.enablePersonalModeText": " Enable Personal Mode for web3 provider. Transaction sent over Web3 will use the web3.personal API.\n", "settings.enablePersonalModeText": " Enable Personal Mode for web3 provider. Transaction sent over Web3 will use the web3.personal API.\n",
"settings.warnText": "Be sure the endpoint is opened before enabling it. This mode allows a user to provide a passphrase in the Remix interface without having to unlock the account. Although this is very convenient, you should completely trust the backend you are connected to (Geth, Parity, ...). Remix never persists any passphrase", "settings.warnText": "Be sure the endpoint is opened before enabling it. This mode allows a user to provide a passphrase in the Remix interface without having to unlock the account. Although this is very convenient, you should completely trust the backend you are connected to (Geth, Parity, ...). Remix never persists any passphrase",
"settings.gitAccessTokenTitle": "GitHub Access Token", "settings.gitAccessTokenTitle": "Github Credentials",
"settings.gitAccessTokenText": "Manage the access token used to publish to Gist and retrieve GitHub contents.", "settings.gitAccessTokenText": "The access token is used to publish a Gist and retrieve GitHub contents. You may need to input username/email.",
"settings.gitAccessTokenText2":"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", "settings.gitAccessTokenText2":"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",
"settings.etherscanTokenTitle": "EtherScan Access Token", "settings.etherscanTokenTitle": "EtherScan Access Token",
"settings.etherscanAccessTokenText": "Manage the api key used to interact with Etherscan.", "settings.etherscanAccessTokenText": "Manage the api key used to interact with Etherscan.",

@ -129,13 +129,16 @@ sub {
sup { sup {
top:-.5em top:-.5em
} }
i {
color: #074438;
}
a { a {
color:#18bc9c; color:#074438;
text-decoration:none; text-decoration:none;
background-color:transparent background-color:transparent
} }
a:hover { a:hover {
color:#0f7864; color:#10947c;
text-decoration:underline text-decoration:underline
} }
a:not([href]):not([class]) { a:not([href]):not([class]) {
@ -182,8 +185,10 @@ th {
text-align:-webkit-match-parent text-align:-webkit-match-parent
} }
label { label {
display:inline-block; display: inline-block;
margin-bottom:.5rem margin-bottom: 0.5rem;
font-size: 11px;
line-height: 12px;
} }
button { button {
border-radius:0 border-radius:0
@ -2872,10 +2877,10 @@ input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-
position:relative; margin-bottom:0; vertical-align:top position:relative; margin-bottom:0; vertical-align:top
} }
.custom-control-label::before { .custom-control-label::before {
position:absolute; top:.203125rem; left:-1.5rem; display:block; width:1rem; height:1rem; pointer-events:none; content:""; background-color:#fff; border:#b4bcc2 solid 1px position:absolute; top:0px; left:-1.5rem; display:block; width:1rem; height:1rem; pointer-events:none; content:""; background-color:#fff; border:#b4bcc2 solid 1px
} }
.custom-control-label::after { .custom-control-label::after {
position:absolute; top:.203125rem; left:-1.5rem; display:block; width:1rem; height:1rem; content:""; background:no-repeat 50%/50% 50% position:absolute; top:0px; left:-1.5rem; display:block; width:1rem; height:1rem; content:""; background:no-repeat 50%/50% 50%
} }
.custom-checkbox .custom-control-label::before { .custom-checkbox .custom-control-label::before {
border-radius:.25rem border-radius:.25rem
@ -7175,7 +7180,7 @@ a.text-dark:focus,a.text-dark:hover {
border:none; color:#fff border:none; color:#fff
} }
.alert .alert-link,.alert a { .alert .alert-link,.alert a {
color:#fff; text-decoration:underline color:#18bc9c; text-decoration:underline
} }
.alert-primary { .alert-primary {
background-color:#2c3e50 background-color:#2c3e50

@ -6,6 +6,7 @@ const version = require('../../package.json').version
const fs = require('fs') const fs = require('fs')
const TerserPlugin = require("terser-webpack-plugin") const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
const axios = require('axios')
const versionData = { const versionData = {
version: version, version: version,
@ -13,8 +14,33 @@ const versionData = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development' mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
} }
const loadLocalSolJson = async () => {
let url = 'https://binaries.soliditylang.org/wasm/list.json'
axios({
url: url,
method: 'GET',
}).then((response) => {
let info = response.data;
info.builds = info.builds.filter(build => build.path.indexOf('nightly') === -1)
info.builds = info.builds.slice(-1)
const buildurl = `https://solc-bin.ethereum.org/wasm/${info.builds[0].path}`;
console.log(`Copying... ${buildurl} to assets`)
const path = `./apps/remix-ide/src/assets/js/soljson.js`;
axios({
method: 'get',
url: buildurl,
responseType: 'stream'
}).then(function (response) {
response.data.pipe(fs.createWriteStream(path));
})
}
)
}
fs.writeFileSync('./apps/remix-ide/src/assets/version.json', JSON.stringify(versionData)) fs.writeFileSync('./apps/remix-ide/src/assets/version.json', JSON.stringify(versionData))
loadLocalSolJson()
const project = fs.readFileSync('./apps/remix-ide/project.json', 'utf8') const project = fs.readFileSync('./apps/remix-ide/project.json', 'utf8')
const implicitDependencies = JSON.parse(project).implicitDependencies const implicitDependencies = JSON.parse(project).implicitDependencies
@ -120,3 +146,5 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
return config; return config;
}); });

@ -9,7 +9,7 @@ import { BreakpointManager } from '../src/code/breakpointManager'
const compiler = require('solc') const compiler = require('solc')
const vmCall = require('./vmCall') const vmCall = require('./vmCall')
const ballot = `pragma solidity >=0.4.22 <0.8.0; const ballot = `pragma solidity >=0.4.22;
/** /**
* @title Ballot * @title Ballot
@ -287,7 +287,7 @@ function testDebugging (debugManager) {
breakPointManager.add({fileName: 'test.sol', row: 39}) breakPointManager.add({fileName: 'test.sol', row: 39})
breakPointManager.event.register('breakpointHit', function (sourceLocation, step) { breakPointManager.event.register('breakpointHit', function (sourceLocation, step) {
t.equal(JSON.stringify(sourceLocation), JSON.stringify({ start: 1153, length: 6, file: 0, jump: '-' })) t.equal(JSON.stringify(sourceLocation), JSON.stringify({ start: 1146, length: 6, file: 0, jump: '-' }))
t.equal(step, 212) t.equal(step, 212)
}) })

@ -95,7 +95,7 @@ tape('SourceLocationTracker', function (t) {
map = await sourceLocationTracker.getValidSourceLocationFromVMTraceIndex('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', 45, output.contracts) map = await sourceLocationTracker.getValidSourceLocationFromVMTraceIndex('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', 45, output.contracts)
st.equal(map['file'], 1) // 1 refers to the generated source (pragma experimental ABIEncoderV2) st.equal(map['file'], 1) // 1 refers to the generated source (pragma experimental ABIEncoderV2)
st.equal(map['start'], 1293) st.equal(map['start'], 1297)
st.equal(map['length'], 32) st.equal(map['length'], 32)
map = await sourceLocationTracker.getValidSourceLocationFromVMTraceIndex('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', 36, output.contracts) map = await sourceLocationTracker.getValidSourceLocationFromVMTraceIndex('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', 36, output.contracts)

@ -19,7 +19,7 @@
"dependencies": { "dependencies": {
"@ethereumjs/util": "^8.0.5", "@ethereumjs/util": "^8.0.5",
"async": "^2.1.2", "async": "^2.1.2",
"ethers": "^4.0.40", "ethers": "^5.7.2",
"ethjs-util": "^0.1.6", "ethjs-util": "^0.1.6",
"events": "^3.0.0", "events": "^3.0.0",
"solc": "^0.7.4", "solc": "^0.7.4",

@ -58,6 +58,7 @@
"express-ws": "^4.0.0", "express-ws": "^4.0.0",
"merge": "^1.2.0", "merge": "^1.2.0",
"signale": "^1.4.0", "signale": "^1.4.0",
"solc": "^0.7.4",
"string-similarity": "^4.0.4", "string-similarity": "^4.0.4",
"time-stamp": "^2.2.0", "time-stamp": "^2.2.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",

@ -157,7 +157,8 @@ export const DropdownPanel = (props: DropdownPanelProps) => {
...prevState.dropdownContent, ...prevState.dropdownContent,
display: 'block' display: 'block'
}, },
copiableContent: JSON.stringify(calldata, null, '\t'), // replace 0xNaN with 0x0
copiableContent: JSON.stringify(calldata, null, '\t').replace(/0xNaN/g, '0x0'),
message: { message: {
innerText: isEmpty ? 'No data available' : '', innerText: isEmpty ? 'No data available' : '',
display: isEmpty ? 'block' : 'none' display: isEmpty ? 'block' : 'none'

@ -946,7 +946,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
</div>} </div>}
> >
<div className="d-flex align-items-center justify-content-center"> <div className="d-flex align-items-center justify-content-center">
{ <i ref={compileIcon} className="fas fa-sync remixui_iconbtn ml-2" aria-hidden="true"></i> } { <i ref={compileIcon} className="fas fa-sync mr-2" aria-hidden="true"></i> }
<div className="text-truncate overflow-hidden text-nowrap" <div className="text-truncate overflow-hidden text-nowrap"
> >
<span> <span>

@ -721,7 +721,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
tooltipId="generateTestsButtontooltip" tooltipId="generateTestsButtontooltip"
tooltipClasses="text-nowrap" tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='solidityUnitTesting.generateTestsButtonTooltip' />} tooltipText={<FormattedMessage id='solidityUnitTesting.generateTestsButtonTooltip' />}
placement={'bottom-start'} placement={'top'}
> >
<button <button
className="btn border w-50" className="btn border w-50"
@ -739,7 +739,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
tooltipId="generateTestsLinktooltip" tooltipId="generateTestsLinktooltip"
tooltipClasses="text-nowrap" tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='solidityUnitTesting.generateTestsLinkTooltip' />} tooltipText={<FormattedMessage id='solidityUnitTesting.generateTestsLinkTooltip' />}
placement={'bottom-start'} placement={'top'}
> >
<a className="btn border text-decoration-none pr-0 d-flex w-50 ml-2" target="__blank" href="https://remix-ide.readthedocs.io/en/latest/unittesting.html#test-directory"> <a className="btn border text-decoration-none pr-0 d-flex w-50 ml-2" target="__blank" href="https://remix-ide.readthedocs.io/en/latest/unittesting.html#test-directory">
<label className="btn p-1 ml-2 m-0"><FormattedMessage id='solidityUnitTesting.howToUse' /></label> <label className="btn p-1 ml-2 m-0"><FormattedMessage id='solidityUnitTesting.howToUse' /></label>
@ -748,7 +748,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
</div> </div>
<div className="d-flex p-2"> <div className="d-flex p-2">
<CustomTooltip <CustomTooltip
placement={'top-start'} placement={'top'}
tooltipClasses="text-nowrap" tooltipClasses="text-nowrap"
tooltipId="info-recorder" tooltipId="info-recorder"
tooltipText={runButtonTitle} tooltipText={runButtonTitle}
@ -758,19 +758,19 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
<label className="labelOnBtn btn btn-primary p-1 ml-2 m-0"><FormattedMessage id='solidityUnitTesting.run' /></label> <label className="labelOnBtn btn btn-primary p-1 ml-2 m-0"><FormattedMessage id='solidityUnitTesting.run' /></label>
</button> </button>
</CustomTooltip> </CustomTooltip>
<button id="runTestsTabStopAction" data-id="testTabRunTestsTabStopAction" className="w-50 pl-2 ml-2 btn btn-secondary" disabled={disableStopButton} onClick={stopTests}>
<CustomTooltip <CustomTooltip
placement={'top-start'} placement={'top'}
tooltipClasses="text-nowrap" tooltipClasses="text-nowrap"
tooltipId="info-recorder" tooltipId="info-recorder"
tooltipText={<FormattedMessage id='solidityUnitTesting.runTestsTabStopActionTooltip' />} tooltipText={<FormattedMessage id='solidityUnitTesting.runTestsTabStopActionTooltip' />}
> >
<button id="runTestsTabStopAction" data-id="testTabRunTestsTabStopAction" className="w-50 pl-2 ml-2 btn btn-secondary" disabled={disableStopButton} onClick={stopTests}>
<span> <span>
<span className="fas fa-stop ml-2"></span> <span className="fas fa-stop ml-2"></span>
<label className="labelOnBtn btn btn-secondary p-1 ml-2 m-0" id="runTestsTabStopActionLabel">{stopButtonLabel}</label> <label className="labelOnBtn btn btn-secondary p-1 ml-2 m-0" id="runTestsTabStopActionLabel">{stopButtonLabel}</label>
</span> </span>
</CustomTooltip>
</button> </button>
</CustomTooltip>
</div> </div>
<div className="d-flex align-items-center mx-3 pb-2 mt-2 border-bottom"> <div className="d-flex align-items-center mx-3 pb-2 mt-2 border-bottom">
<input id="checkAllTests" <input id="checkAllTests"

@ -97,7 +97,7 @@ export const createWorkspace = async (workspaceName: string, workspaceTemplateNa
if (!currentBranch) { if (!currentBranch) {
if (!name || !email) { if (!name || !email) {
await plugin.call('notification', 'toast', 'Please add username and email to Remix GitHub Settings to use git features.') await plugin.call('notification', 'toast', 'To use Git features, add username and email to the Github section of the Settings panel.')
} else { } else {
// commit the template as first commit // commit the template as first commit
plugin.call('notification', 'toast', 'Creating initial git commit ...') plugin.call('notification', 'toast', 'Creating initial git commit ...')

@ -1,9 +1,10 @@
import React, { useRef, useEffect } from 'react' // eslint-disable-line import React, { useRef, useEffect, useState } from 'react' // eslint-disable-line
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import { action, FileExplorerContextMenuProps } from '../types' import { action, FileExplorerContextMenuProps } from '../types'
import '../css/file-explorer-context-menu.css' import '../css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api' import { customAction } from '@remixproject/plugin-api'
import UploadFile from './upload-file'
declare global { declare global {
interface Window { interface Window {
@ -13,9 +14,11 @@ declare global {
const _paq = window._paq = window._paq || [] //eslint-disable-line const _paq = window._paq = window._paq || [] //eslint-disable-line
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, downloadPath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, copyFileName, copyPath, paste, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, copyFileName, copyPath, paste, runScript, emit, pageX, pageY, path, type, focus, downloadPath, uploadFile,...otherProps } = props
const contextMenuRef = useRef(null) const contextMenuRef = useRef(null)
const intl = useIntl() const intl = useIntl()
const [showFileExplorer, setShowFileExplorer] = useState(false)
useEffect(() => { useEffect(() => {
contextMenuRef.current.focus() contextMenuRef.current.focus()
}, []) }, [])
@ -37,6 +40,11 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
* for example : 'downloadAsZip' with type ['file','folder'] will work on files and folders when multiple are selected * for example : 'downloadAsZip' with type ['file','folder'] will work on files and folders when multiple are selected
**/ **/
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') }) const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') })
if(focus[0].key === "contextMenu"){
return true
}
if (nonRootFocus.length > 1) { if (nonRootFocus.length > 1) {
for (const element of nonRootFocus) { for (const element of nonRootFocus) {
if (!itemMatchesCondition(item, element.type, element.key)) return false if (!itemMatchesCondition(item, element.type, element.key)) return false
@ -65,6 +73,29 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
const menu = () => { const menu = () => {
return actions.filter(item => filterItem(item)).map((item, index) => { return actions.filter(item => filterItem(item)).map((item, index) => {
if(item.name === "Upload File"){
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
className='remixui_liitem'
onClick={()=>{
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'uploadFile'])
setShowFileExplorer(true)
}}
>{intl.formatMessage({id: `filePanel.${item.id}`, defaultMessage: item.label || item.name})}</li>
}
if(item.name === "Load a Local File"){
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
className='remixui_liitem'
onClick={()=>{
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'uploadFile'])
setShowFileExplorer(true)
}}
>{intl.formatMessage({id: `filePanel.${item.id}`, defaultMessage: item.label || item.name})}</li>
}
return <li return <li
id={`menuitem${item.name.toLowerCase()}`} id={`menuitem${item.name.toLowerCase()}`}
key={index} key={index}
@ -128,6 +159,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
deletePath(getPath()) deletePath(getPath())
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'deleteAll']) _paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'deleteAll'])
break break
case 'Publish Workspace to Gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace'])
publishFolderToGist(path, type)
break
default: default:
_paq.push(['trackEvent', 'fileExplorer', 'customAction', `${item.id}/${item.name}`]) _paq.push(['trackEvent', 'fileExplorer', 'customAction', `${item.id}/${item.name}`])
emit && emit({ ...item, path: [path] } as customAction) emit && emit({ ...item, path: [path] } as customAction)
@ -148,6 +183,8 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
tabIndex={500} tabIndex={500}
{...otherProps} {...otherProps}
> >
{showFileExplorer && <UploadFile onUpload={(target)=> {
uploadFile(target); }} multiple />}
<ul id='remixui_menuitems'>{menu()}</ul> <ul id='remixui_menuitems'>{menu()}</ul>
</div> </div>
) )

@ -1,5 +1,5 @@
import { CustomTooltip } from '@remix-ui/helper' import { CustomTooltip } from '@remix-ui/helper'
import React, { useState, useEffect } from 'react' //eslint-disable-line import React, { useState, useEffect, } from 'react' //eslint-disable-line
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
import { Placement } from 'react-bootstrap/esm/Overlay' import { Placement } from 'react-bootstrap/esm/Overlay'
import { FileExplorerMenuProps } from '../types' import { FileExplorerMenuProps } from '../types'

@ -2,9 +2,7 @@ import React, { useEffect, useState, useRef, SyntheticEvent } from 'react' // es
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, MenuItems, FileExplorerState } from '../types' import { FileExplorerProps, WorkSpaceState } from '../types'
import { customAction } from '@remixproject/plugin-api'
import { contextMenuActions } from '../utils'
import '../css/file-explorer.css' import '../css/file-explorer.css'
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper' import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper'
@ -14,29 +12,9 @@ import { Drag } from "@remix-ui/drag-n-drop"
import { ROOT_PATH } from '../utils/constants' import { ROOT_PATH } from '../utils/constants'
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props const { name, contextMenuItems, removedContextMenuItems, files, workspaceState, toGist, addMenuItems,
const [state, setState] = useState<FileExplorerState>({ removeMenuItems, handleContextMenu, handleNewFileInput, handleNewFolderInput, uploadFile, uploadFolder, fileState } = props
ctrlKey: false, const [state, setState] = useState<WorkSpaceState>( workspaceState)
newFileName: '',
actions: contextMenuActions,
focusContext: {
element: null,
x: null,
y: null,
type: ''
},
focusEdit: {
element: null,
type: '',
isNew: false,
lastEdit: ''
},
mouseOverElement: null,
showContextMenu: false,
reservedKeywords: [ROOT_PATH, 'gist-'],
copyElement: []
})
const [canPaste, setCanPaste] = useState(false)
const treeRef = useRef<HTMLDivElement>(null) const treeRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
@ -59,6 +37,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, [props.focusEdit]) }, [props.focusEdit])
useEffect(() => {
setState(workspaceState)
}, [workspaceState])
useEffect(() => { useEffect(() => {
if (treeRef.current) { if (treeRef.current) {
const keyPressHandler = (e: KeyboardEvent) => { const keyPressHandler = (e: KeyboardEvent) => {
@ -87,62 +69,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, [treeRef.current]) }, [treeRef.current])
useEffect(() => {
if (canPaste) {
addMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
} else {
removeMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
}
}, [canPaste])
const addMenuItems = (items: MenuItems) => {
setState(prevState => {
// filter duplicate items
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...actions] }
})
}
const removeMenuItems = (items: MenuItems) => {
setState(prevState => {
const actions = prevState.actions.filter(({ id, name }) => items.findIndex(item => id === item.id && name === item.name) === -1)
return { ...prevState, actions }
})
}
const hasReservedKeyword = (content: string): boolean => { const hasReservedKeyword = (content: string): boolean => {
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true
else return false else return false
} }
const getFocusedFolder = () => {
if (props.focusElement[0]) {
if (props.focusElement[0].type === 'folder' && props.focusElement[0].key) return props.focusElement[0].key
else if (props.focusElement[0].type === 'gist' && props.focusElement[0].key) return props.focusElement[0].key
else if (props.focusElement[0].type === 'file' && props.focusElement[0].key) return extractParentFromKey(props.focusElement[0].key) ? extractParentFromKey(props.focusElement[0].key) : ROOT_PATH
else return ROOT_PATH
}
}
const createNewFile = async (newFilePath: string) => { const createNewFile = async (newFilePath: string) => {
try { try {
props.dispatchCreateNewFile(newFilePath, ROOT_PATH) props.dispatchCreateNewFile(newFilePath, ROOT_PATH)
@ -159,13 +90,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
} }
const deletePath = async (path: string[]) => {
if (props.readonly) return props.toast('cannot delete file. ' + name + ' is a read only explorer')
if (!Array.isArray(path)) path = [path]
props.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { props.dispatchDeletePath(path) }, 'Cancel', () => {})
}
const renamePath = async (oldPath: string, newPath: string) => { const renamePath = async (oldPath: string, newPath: string) => {
try { try {
props.dispatchRenamePath(oldPath, newPath) props.dispatchRenamePath(oldPath, newPath)
@ -174,81 +98,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
} }
const downloadPath = async (path: string) => {
try {
props.dispatchDownloadPath(path)
} catch (error) {
props.modal('Download Failed', 'Unexpected error while downloading: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
const uploadFile = (target) => {
const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...props.expandPath, parentFolder])]
props.dispatchHandleExpandPath(expandPath)
props.dispatchUploadFile(target, parentFolder)
}
const uploadFolder = (target) => {
const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...props.expandPath, parentFolder])]
props.dispatchHandleExpandPath(expandPath)
props.dispatchUploadFolder(target, parentFolder)
}
const copyFile = (src: string, dest: string) => {
try {
props.dispatchCopyFile(src, dest)
} catch (error) {
props.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {})
}
}
const copyFolder = (src: string, dest: string) => {
try {
props.dispatchCopyFolder(src, dest)
} catch (error) {
props.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {})
}
}
const publishToGist = (path?: string, type?: string) => { const publishToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
} }
const pushChangesToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFolderToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFileToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const toGist = (path?: string, type?: string) => {
props.dispatchPublishToGist(path, type)
}
const runScript = async (path: string) => {
try {
props.dispatchRunScript(path)
} catch (error) {
props.toast('Run script failed')
}
}
const emitContextMenuEvent = (cmd: customAction) => {
try {
props.dispatchEmitContextMenuEvent(cmd)
} catch (error) {
props.toast(error)
}
}
const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => { const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => {
if (!state.ctrlKey) { if (!state.ctrlKey) {
@ -294,26 +147,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
} }
const handleContextMenu = (pageX: number, pageY: number, path: string, content: string, type: string) => {
if (!content) return
setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path }
})
}
const hideContextMenu = () => {
setState(prevState => {
return { ...prevState, focusContext: { element: null, x: 0, y: 0, type: '' }, showContextMenu: false }
})
}
const editModeOn = (path: string, type: string, isNew = false) => {
if (props.readonly) return props.toast('Cannot write/modify file system in read only mode.')
setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
})
}
const editModeOff = async (content: string) => { const editModeOff = async (content: string) => {
if (typeof content === 'string') content = content.trim() if (typeof content === 'string') content = content.trim()
const parentFolder = extractParentFromKey(state.focusEdit.element) const parentFolder = extractParentFromKey(state.focusEdit.element)
@ -366,50 +199,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
} }
const handleNewFileInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
const expandPath = [...new Set([...props.expandPath, parentFolder])]
await props.dispatchAddInputField(parentFolder, 'file')
props.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'file', true)
}
const handleNewFolderInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder)
const expandPath = [...new Set([...props.expandPath, parentFolder])]
await props.dispatchAddInputField(parentFolder, 'folder')
props.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'folder', true)
}
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file') => {
setState(prevState => {
return { ...prevState, copyElement: [{ key: path, type }] }
})
setCanPaste(true)
props.toast(`Copied to clipboard ${path}`)
}
const handlePasteClick = (dest: string, destType: string) => {
dest = destType === 'file' ? extractParentFromKey(dest) || ROOT_PATH : dest
state.copyElement.map(({ key, type }) => {
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest)
})
}
const deleteMessage = (path: string[]) => {
return (
<div>
<div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div>
{
path.map((item, i) => (<li key={i}>{item}</li>))
}
</div>
)
}
const handleFileExplorerMenuClick = (e: SyntheticEvent) => { const handleFileExplorerMenuClick = (e: SyntheticEvent) => {
e.stopPropagation() e.stopPropagation()
@ -425,17 +214,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
props.dispatchHandleExpandPath(expandPath) props.dispatchHandleExpandPath(expandPath)
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFileNameClick = (path: string, _type: string) => {
const fileName = extractNameFromKey(path)
navigator.clipboard.writeText(fileName)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFilePathClick = (path: string, _type: string) => {
navigator.clipboard.writeText(path)
}
const handleFileMove = (dest: string, src: string) => { const handleFileMove = (dest: string, src: string) => {
try { try {
props.dispatchMoveFile(src, dest) props.dispatchMoveFile(src, dest)
@ -491,38 +269,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
key={index} key={index}
showIconsMenu={props.showIconsMenu} showIconsMenu={props.showIconsMenu}
hideIconsMenu={props.hideIconsMenu} hideIconsMenu={props.hideIconsMenu}
/>) />)
} }
</TreeView> </TreeView>
</div> </div>
</TreeViewItem> </TreeViewItem>
</TreeView> </TreeView>
{ state.showContextMenu &&
<FileExplorerContextMenu
actions={props.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)}
hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
downloadPath={downloadPath}
renamePath={editModeOn}
runScript={runScript}
copy={handleCopyClick}
paste={handlePasteClick}
copyFileName={handleCopyFileNameClick}
copyPath={handleCopyFilePathClick}
emit={emitContextMenuEvent}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
path={state.focusContext.element}
type={state.focusContext.type}
focus={props.focusElement}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
/>
}
</div> </div>
</Drag> </Drag>
) )

@ -0,0 +1,31 @@
import React, { useEffect, useRef } from "react";
type UploadFileProps = {
onUpload: (target: EventTarget & HTMLInputElement, files?: FileList) => void;
accept?: string;
multiple?: boolean;
};
const UploadFile = (props: UploadFileProps) => {
const ref = useRef<HTMLInputElement>();
useEffect(() => {
ref.current.click();
ref.current.onchange= (event)=>{
//@ts-ignore
props.onUpload(event.target, event.target.files);
}
}, []);
return (
<input
ref={ref}
style={{ display: "none" }}
accept={props.accept}
multiple={props.multiple}
type="file"
/>
);
};
export default UploadFile;

@ -1,12 +1,18 @@
import React, { useState, useEffect, useRef, useContext, SyntheticEvent, ChangeEvent, KeyboardEvent } from 'react' // eslint-disable-line import React, { useState, useEffect, useRef, useContext, SyntheticEvent, ChangeEvent, KeyboardEvent, MouseEvent } from 'react' // eslint-disable-line
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import { Dropdown } from 'react-bootstrap' import { Dropdown } from 'react-bootstrap'
import { CustomIconsToggle, CustomMenu, CustomToggle, CustomTooltip } from '@remix-ui/helper' import { CustomIconsToggle, CustomMenu, CustomToggle, CustomTooltip, extractNameFromKey, extractParentFromKey } from '@remix-ui/helper'
import { FileExplorer } from './components/file-explorer' // eslint-disable-line import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import { FileSystemContext } from './contexts' import { FileSystemContext } from './contexts'
import './css/remix-ui-workspace.css' import './css/remix-ui-workspace.css'
import { ROOT_PATH, TEMPLATE_NAMES } from './utils/constants' import { ROOT_PATH, TEMPLATE_NAMES } from './utils/constants'
import { HamburgerMenu } from './components/workspace-hamburger' import { HamburgerMenu } from './components/workspace-hamburger'
import { MenuItems, WorkSpaceState } from './types'
import { contextMenuActions } from './utils'
import FileExplorerContextMenu from './components/file-explorer-context-menu'
import { customAction } from '@remixproject/plugin-api'
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const canUpload = window.File || window.FileReader || window.FileList || window.Blob const canUpload = window.File || window.FileReader || window.FileList || window.Blob
@ -36,6 +42,56 @@ export function Workspace () {
const filteredBranches = selectedWorkspace ? (selectedWorkspace.branches || []).filter(branch => branch.name.includes(branchFilter) && branch.name !== 'HEAD').slice(0, 20) : [] const filteredBranches = selectedWorkspace ? (selectedWorkspace.branches || []).filter(branch => branch.name.includes(branchFilter) && branch.name !== 'HEAD').slice(0, 20) : []
const currentBranch = selectedWorkspace ? selectedWorkspace.currentBranch : null const currentBranch = selectedWorkspace ? selectedWorkspace.currentBranch : null
const [canPaste, setCanPaste] = useState(false)
const [state, setState] = useState<WorkSpaceState>({
ctrlKey: false,
newFileName: '',
actions: contextMenuActions,
focusContext: {
element: null,
x: null,
y: null,
type: ''
},
focusEdit: {
element: null,
type: '',
isNew: false,
lastEdit: ''
},
mouseOverElement: null,
showContextMenu: false,
reservedKeywords: [ROOT_PATH, 'gist-'],
copyElement: []
})
useEffect(() => {
if (canPaste) {
addMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file', 'workspace'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
} else {
removeMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file', 'workspace'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
}
}, [canPaste])
useEffect(() => { useEffect(() => {
let workspaceName = localStorage.getItem('currentWorkspace') let workspaceName = localStorage.getItem('currentWorkspace')
if (!workspaceName && global.fs.browser.workspaces.length) { if (!workspaceName && global.fs.browser.workspaces.length) {
@ -114,6 +170,23 @@ export function Workspace () {
) )
} }
const addMenuItems = (items: MenuItems) => {
setState(prevState => {
// filter duplicate items
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...actions] }
})
}
const removeMenuItems = (items: MenuItems) => {
setState(prevState => {
const actions = prevState.actions.filter(({ id, name }) => items.findIndex(item => id === item.id && name === item.name) === -1)
return { ...prevState, actions }
})
}
const cloneGitRepository = () => { const cloneGitRepository = () => {
global.modal( global.modal(
intl.formatMessage({ id: 'filePanel.workspace.clone' }), intl.formatMessage({ id: 'filePanel.workspace.clone' }),
@ -271,6 +344,174 @@ export function Workspace () {
} }
} }
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file' | 'workspace') => {
setState(prevState => {
return { ...prevState, copyElement: [{ key: path, type }] }
})
setCanPaste(true)
global.toast(`Copied to clipboard ${path}`)
}
const handlePasteClick = (dest: string, destType: string) => {
dest = destType === 'file' ? extractParentFromKey(dest) || ROOT_PATH : dest
state.copyElement.map(({ key, type }) => {
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest)
})
}
const downloadPath = async (path: string) => {
try {
global.dispatchDownloadPath(path)
} catch (error) {
global.modal('Download Failed', 'Unexpected error while downloading: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
const copyFile = (src: string, dest: string) => {
try {
global.dispatchCopyFile(src, dest)
} catch (error) {
global.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {})
}
}
const copyFolder = (src: string, dest: string) => {
try {
global.dispatchCopyFolder(src, dest)
} catch (error) {
global.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {})
}
}
const handleContextMenu = (pageX: number, pageY: number, path: string, content: string, type: string) => {
if (!content) return
setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path }
})
}
const getFocusedFolder = () => {
const focusElement = global.fs.focusElement
if (focusElement[0]) {
if (focusElement[0].type === 'folder' && focusElement[0].key) return focusElement[0].key
else if (focusElement[0].type === 'gist' && focusElement[0].key) return focusElement[0].key
else if (focusElement[0].type === 'file' && focusElement[0].key) return extractParentFromKey(focusElement[0].key) ? extractParentFromKey(focusElement[0].key) : ROOT_PATH
else return ROOT_PATH
}
}
const uploadFile = (target) => {
const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...global.fs.browser.expandPath, parentFolder])]
global.dispatchHandleExpandPath(expandPath)
global.dispatchUploadFile(target, parentFolder)
}
const uploadFolder = (target) => {
const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...global.fs.browser.expandPath, parentFolder])]
global.dispatchHandleExpandPath(expandPath)
global.dispatchUploadFolder(target, parentFolder)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFileNameClick = (path: string, _type: string) => {
const fileName = extractNameFromKey(path)
navigator.clipboard.writeText(fileName)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFilePathClick = (path: string, _type: string) => {
navigator.clipboard.writeText(path)
}
const hideContextMenu = () => {
setState(prevState => {
return { ...prevState, focusContext: { element: null, x: 0, y: 0, type: '' }, showContextMenu: false }
})
}
const runScript = async (path: string) => {
try {
global.dispatchRunScript(path)
} catch (error) {
global.toast('Run script failed')
}
}
const emitContextMenuEvent = (cmd: customAction) => {
try {
global.dispatchEmitContextMenuEvent(cmd)
} catch (error) {
global.toast(error)
}
}
const pushChangesToGist = (path?: string, type?: string) => {
global.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFolderToGist = (path?: string, type?: string) => {
global.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFileToGist = (path?: string, type?: string) => {
global.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const deleteMessage = (path: string[]) => {
return (
<div>
<div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div>
{
path.map((item, i) => (<li key={i}>{item}</li>))
}
</div>
)
}
const deletePath = async (path: string[]) => {
if (global.fs.readonly) return global.toast('cannot delete file. ' + name + ' is a read only explorer')
if (!Array.isArray(path)) path = [path]
global.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { global.dispatchDeletePath(path) }, 'Cancel', () => {})
}
const toGist = (path?: string, type?: string) => {
global.dispatchPublishToGist(path, type)
}
const editModeOn = (path: string, type: string, isNew = false) => {
if (global.fs.readonly) return global.toast('Cannot write/modify file system in read only mode.')
setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
})
}
const handleNewFileInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
const expandPath = [...new Set([...global.fs.browser.expandPath, parentFolder])]
await global.dispatchAddInputField(parentFolder, 'file')
global.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'file', true)
}
const handleNewFolderInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder)
const expandPath = [...new Set([...global.fs.browser.expandPath, parentFolder])]
await global.dispatchAddInputField(parentFolder, 'folder')
global.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'folder', true)
}
const toggleDropdown = (isOpen: boolean) => { const toggleDropdown = (isOpen: boolean) => {
setShowDropdown(isOpen) setShowDropdown(isOpen)
} }
@ -428,8 +669,12 @@ export function Workspace () {
return ( return (
<div className='d-flex flex-column justify-content-between h-100'> <div className='d-flex flex-column justify-content-between h-100'>
<div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }}> <div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }} onContextMenu={(e)=>{
<div className='d-flex flex-column w-100 mb-1 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}> e.preventDefault()
handleContextMenu(e.pageX, e.pageY, ROOT_PATH, "workspace", 'workspace')
}
}>
<div className='d-flex flex-column w-100 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div> <div>
<header> <header>
<div className="mx-2 mb-2 d-flex flex-column"> <div className="mx-2 mb-2 d-flex flex-column">
@ -455,7 +700,7 @@ export function Workspace () {
createWorkspace() createWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate']) _paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate'])
}} }}
style={{ fontSize: 'large' }} style={{ fontSize: 'medium' }}
className='far fa-plus remixui_menuicon d-flex align-self-end' className='far fa-plus remixui_menuicon d-flex align-self-end'
> >
</span> </span>
@ -537,19 +782,20 @@ export function Workspace () {
</div> </div>
</header> </header>
</div> </div>
<div className='h-100 mb-4 pb-4 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}> <div className='h-100 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}>
<div className='h-100'> <div className='h-100'>
{ (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>} { (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>}
{ !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && { !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) &&
(global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'> <div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'>
<FileExplorer <FileExplorer
fileState={global.fs.browser.fileState}
name={currentWorkspace} name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']} menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems} contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems} removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files} files={global.fs.browser.files}
fileState={global.fs.browser.fileState} workspaceState={state}
expandPath={global.fs.browser.expandPath} expandPath={global.fs.browser.expandPath}
focusEdit={global.fs.focusEdit} focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement} focusElement={global.fs.focusElement}
@ -578,12 +824,24 @@ export function Workspace () {
dispatchHandleExpandPath={global.dispatchHandleExpandPath} dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile} dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder} dispatchMoveFolder={global.dispatchMoveFolder}
handleCopyClick={handleCopyClick}
handlePasteClick={handlePasteClick}
addMenuItems={addMenuItems}
removeMenuItems={removeMenuItems}
handleContextMenu={handleContextMenu}
uploadFile={uploadFile}
uploadFolder={uploadFolder}
getFocusedFolder={getFocusedFolder}
toGist={toGist}
editModeOn={editModeOn}
handleNewFileInput={handleNewFileInput}
handleNewFolderInput={handleNewFolderInput}
/> />
</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> }
{ (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) && { (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) &&
<div className='h-100 filesystemexplorer pb-4 mb-4 remixui_treeview'> <div className='h-100 filesystemexplorer remixui_treeview'>
<FileExplorer <FileExplorer
name='localhost' name='localhost'
menuItems={['createNewFile', 'createNewFolder']} menuItems={['createNewFile', 'createNewFolder']}
@ -591,6 +849,7 @@ export function Workspace () {
removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems} removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
files={global.fs.localhost.files} files={global.fs.localhost.files}
fileState={[]} fileState={[]}
workspaceState={state}
expandPath={global.fs.localhost.expandPath} expandPath={global.fs.localhost.expandPath}
focusEdit={global.fs.focusEdit} focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement} focusElement={global.fs.focusElement}
@ -619,6 +878,18 @@ export function Workspace () {
dispatchHandleExpandPath={global.dispatchHandleExpandPath} dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile} dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder} dispatchMoveFolder={global.dispatchMoveFolder}
handleCopyClick={handleCopyClick}
handlePasteClick={handlePasteClick}
addMenuItems={addMenuItems}
removeMenuItems={removeMenuItems}
handleContextMenu={handleContextMenu}
uploadFile={uploadFile}
uploadFolder={uploadFolder}
getFocusedFolder={getFocusedFolder}
toGist={toGist}
editModeOn={editModeOn}
handleNewFileInput={handleNewFileInput}
handleNewFolderInput={handleNewFolderInput}
/> />
</div> </div>
} }
@ -685,6 +956,32 @@ export function Workspace () {
</div> </div>
</div> </div>
} }
{state.showContextMenu && <FileExplorerContextMenu
actions={global.fs.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)}
hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
renamePath={editModeOn}
runScript={runScript}
copy={handleCopyClick}
paste={handlePasteClick}
copyFileName={handleCopyFileNameClick}
copyPath={handleCopyFilePathClick}
emit={emitContextMenuEvent}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
path={state.focusContext.element}
type={state.focusContext.type}
focus={global.fs.focusElement}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
uploadFile={uploadFile}
downloadPath={downloadPath}
/>
}
</div> </div>
) )
} }

@ -5,7 +5,7 @@ import { fileDecoration } from '@remix-ui/file-decorators'
import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types' import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types'
import { ViewPlugin } from '@remixproject/engine-web' import { ViewPlugin } from '@remixproject/engine-web'
export type action = { name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean } export type action = { name: string, type?: Array<'folder' | 'gist' | 'file' | 'workspace'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean }
export interface JSONStandardInput { export interface JSONStandardInput {
language: "Solidity"; language: "Solidity";
settings?: any, settings?: any,
@ -82,6 +82,7 @@ export interface FileExplorerProps {
contextMenuItems: MenuItems, contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems, removedContextMenuItems: MenuItems,
files: { [x: string]: Record<string, FileType> }, files: { [x: string]: Record<string, FileType> },
workspaceState: WorkSpaceState,
fileState: fileDecoration[], fileState: fileDecoration[],
expandPath: string[], expandPath: string[],
focusEdit: string, focusEdit: string,
@ -111,7 +112,19 @@ export interface FileExplorerProps {
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>, dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: string[]) => Promise<void>, dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchMoveFile: (src: string, dest: string) => Promise<void>, dispatchMoveFile: (src: string, dest: string) => Promise<void>,
dispatchMoveFolder: (src: string, dest: string) => Promise<void> dispatchMoveFolder: (src: string, dest: string) => Promise<void>,
handlePasteClick: (dest: string, destType: string) => void
handleCopyClick: (path: string, type: 'folder' | 'gist' | 'file' | 'workspace') => void
addMenuItems: (items: MenuItems) => void
removeMenuItems: (items: MenuItems) => void
handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void
uploadFile: (target) => void
uploadFolder: (target) => void
getFocusedFolder: () => string
editModeOn: (path: string, type: string, isNew: boolean) => void
toGist: (path?: string, type?: string) => void
handleNewFileInput: (parentFolder?: string) => Promise<void>
handleNewFolderInput: (parentFolder?: string) => Promise<void>
} }
type Placement = import('react-overlays/usePopper').Placement type Placement = import('react-overlays/usePopper').Placement
export interface FileExplorerMenuProps { export interface FileExplorerMenuProps {
@ -149,27 +162,23 @@ export interface FileExplorerContextMenuProps {
copyFileName?: (path: string, type: string) => void copyFileName?: (path: string, type: string) => void
copyPath?: (path: string, type: string) => void copyPath?: (path: string, type: string) => void
generateUml?: (path: string) => Promise<void> generateUml?: (path: string) => Promise<void>
uploadFile?: (target: EventTarget & HTMLInputElement) => void
} }
export interface FileExplorerState { export interface WorkSpaceState {
ctrlKey: boolean ctrlKey: boolean
newFileName: string newFileName: string
actions: { actions: {
id: string id: string
name: string name: string
type?: Array<'folder' | 'gist' | 'file'> type?: Array<'folder' | 'gist' | 'file' | 'workspace'>
path?: string[] path?: string[]
extension?: string[] extension?: string[]
pattern?: string[] pattern?: string[]
multiselect: boolean multiselect: boolean
label: string label: string
}[] }[]
focusContext: { focusContext: FileFocusContextType
element: string
x: number
y: number
type: string
}
focusEdit: { focusEdit: {
element: string element: string
type: string type: string
@ -179,8 +188,17 @@ export interface FileExplorerState {
mouseOverElement: string mouseOverElement: string
showContextMenu: boolean showContextMenu: boolean
reservedKeywords: string[] reservedKeywords: string[]
copyElement: { copyElement: CopyElementType[]
key: string
type: 'folder' | 'gist' | 'file'
}[]
} }
export type FileFocusContextType = {
element: string
x: number
y: number
type: string
}
export type CopyElementType = {
key: string
type: 'folder' | 'gist' | 'file' | 'workspace'
}

@ -1,15 +1,15 @@
import { MenuItems } from '../types' import { WorkspaceProps, MenuItems } from '../types'
export const contextMenuActions: MenuItems = [{ export const contextMenuActions: MenuItems = [{
id: 'newFile', id: 'newFile',
name: 'New File', name: 'New File',
type: ['folder', 'gist'], type: ['folder', 'gist', 'workspace'],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
id: 'newFolder', id: 'newFolder',
name: 'New Folder', name: 'New Folder',
type: ['folder', 'gist'], type: ['folder', 'gist', 'workspace'],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
@ -27,7 +27,7 @@ export const contextMenuActions: MenuItems = [{
},{ },{
id: 'download', id: 'download',
name: 'Download', name: 'Download',
type: ['file', 'folder'], type: ['file', 'folder', 'workspace'],
multiselect: false, multiselect: false,
label: '' label: ''
}, { }, {
@ -78,4 +78,23 @@ export const contextMenuActions: MenuItems = [{
type: ['folder', 'file'], type: ['folder', 'file'],
multiselect: true, multiselect: true,
label: '' label: ''
},{
id: 'uploadFile',
name: 'Load a Local File',
type: ['folder', 'gist', 'workspace'],
multiselect: false,
label: 'Load a Local File'
}, {
id: 'publishToGist',
name: 'Push changes to gist',
type: ['folder', 'gist'],
multiselect: false,
label: 'Publish all to Gist'
},
{
id: 'publishWorkspace',
name: 'Publish Workspace to Gist',
type: ['workspace'],
multiselect: false,
label: ''
}] }]

@ -21,7 +21,7 @@
"remix-ide": "./apps/remix-ide/bin/remix-ide" "remix-ide": "./apps/remix-ide/bin/remix-ide"
}, },
"engines": { "engines": {
"node": "^14.17.6", "node": "^20.0.0",
"npm": "^6.14.15" "npm": "^6.14.15"
}, },
"scripts": { "scripts": {
@ -48,7 +48,7 @@
"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,remix-ui-file-decorators,remix-ui-tooltip-popup,ghaction-helper", "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-file-decorators,remix-ui-tooltip-popup,ghaction-helper",
"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,ghaction-helper", "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,ghaction-helper",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,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-tests,remix-url-resolver",
"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",
"publishDev:libs": "yarn run build:libs && lerna publish --npm-tag alpha --skip-git && yarn run bumpVersion:libs", "publishDev:libs": "yarn run build:libs && lerna publish --npm-tag alpha --skip-git && yarn run bumpVersion:libs",
"build:e2e": "node apps/remix-ide-e2e/src/buildGroupTests.js && tsc -p apps/remix-ide-e2e/tsconfig.e2e.json", "build:e2e": "node apps/remix-ide-e2e/src/buildGroupTests.js && tsc -p apps/remix-ide-e2e/tsconfig.e2e.json",
@ -58,8 +58,6 @@
"browsertest": "sleep 5 && yarn run nightwatch_local", "browsertest": "sleep 5 && yarn run nightwatch_local",
"csslint": "csslint --ignore=order-alphabetical --errors='errors,duplicate-properties,empty-rules' --exclude-list='apps/remix-ide/src/assets/css/font-awesome.min.css' apps/remix-ide/src/assets/css/", "csslint": "csslint --ignore=order-alphabetical --errors='errors,duplicate-properties,empty-rules' --exclude-list='apps/remix-ide/src/assets/css/font-awesome.min.css' apps/remix-ide/src/assets/css/",
"downloadsolc_assets_e2e": "node ./apps/remix-ide/ci/download_e2e_assets.js", "downloadsolc_assets_e2e": "node ./apps/remix-ide/ci/download_e2e_assets.js",
"downloadsolc_assets": "wget --no-check-certificate https://binaries.soliditylang.org/wasm/soljson-v0.8.18+commit.87f61d96.js -O ./apps/remix-ide/src/assets/js/soljson.js",
"downloadsolc_assets_dist": "wget --no-check-certificate https://binaries.soliditylang.org/wasm/soljson-v0.8.18+commit.87f61d96.js -O ./dist/apps/remix-ide/assets/js/soljson.js",
"make-mock-compiler": "node apps/remix-ide/ci/makeMockCompiler.js", "make-mock-compiler": "node apps/remix-ide/ci/makeMockCompiler.js",
"minify": "uglifyjs --in-source-map inline --source-map-inline -c warnings=false", "minify": "uglifyjs --in-source-map inline --source-map-inline -c warnings=false",
"build:production": "NODE_ENV=production nx build remix-ide --configuration=production --skip-nx-cache", "build:production": "NODE_ENV=production nx build remix-ide --configuration=production --skip-nx-cache",
@ -200,7 +198,6 @@
"sol2uml": "^2.4.3", "sol2uml": "^2.4.3",
"string-similarity": "^4.0.4", "string-similarity": "^4.0.4",
"svg2pdf.js": "^2.2.1", "svg2pdf.js": "^2.2.1",
"swarmgw": "^0.3.1",
"time-stamp": "^2.2.0", "time-stamp": "^2.2.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
@ -310,7 +307,7 @@
"ganache-cli": "^6.8.1", "ganache-cli": "^6.8.1",
"gists": "^1.0.1", "gists": "^1.0.1",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"hardhat": "^2.12.7", "hardhat": "^2.14.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"ipfs-http-client": "^47.0.1", "ipfs-http-client": "^47.0.1",
"ipfs-mini": "^1.1.5", "ipfs-mini": "^1.1.5",
@ -341,7 +338,7 @@
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"selenium-standalone": "^8.2.3", "selenium-standalone": "^8.2.3",
"semver": "^7.4.0", "semver": "^7.4.0",
"solc": "0.7.4", "solc": "^0.7.4",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"stream-http": "^3.2.0", "stream-http": "^3.2.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save