Merge branch 'master' into update-ghaction

pull/5370/head
Aniket 2 years ago committed by GitHub
commit e4339ad539
  1. 37
      .circleci/config.yml
  2. 3
      .github/workflows/publish-action.yml
  3. 2
      README.md
  4. 2
      apps/etherscan/src/app/views/VerifyView.tsx
  5. 2
      apps/remix-ide-e2e/package.json
  6. 1
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  7. 2
      apps/remix-ide-e2e/src/tests/workspace_git.test.ts
  8. 2
      apps/remix-ide/.gitignore
  9. 114
      apps/remix-ide/ci/download_e2e_assets.js
  10. 33
      apps/remix-ide/ci/downloadsoljson.sh
  11. 1
      apps/remix-ide/project.json
  12. 2
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  13. 4
      apps/remix-ide/src/app/tabs/locales/en/settings.json
  14. 19
      apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css
  15. 12
      apps/remix-ide/webpack.config.js
  16. 2
      libs/remix-analyzer/package.json
  17. 4
      libs/remix-debug/test/debugger.ts
  18. 2
      libs/remix-debug/test/sourceLocationTracker.ts
  19. 2
      libs/remix-lib/package.json
  20. 2
      libs/remix-solidity/package.json
  21. 3
      libs/remix-tests/package.json
  22. 3
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/dropdown-panel.tsx
  23. 2
      libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx
  24. 32
      libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
  25. 2
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  26. 43
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  27. 2
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  28. 352
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  29. 31
      libs/remix-ui/workspace/src/lib/components/upload-file.tsx
  30. 315
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  31. 46
      libs/remix-ui/workspace/src/lib/types/index.ts
  32. 29
      libs/remix-ui/workspace/src/lib/utils/index.ts
  33. 2
      libs/remix-url-resolver/package.json
  34. 2
      libs/remixd/package.json
  35. 15
      package.json
  36. 1134
      yarn.lock

@ -9,7 +9,7 @@ orbs:
jobs:
build:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
@ -24,16 +24,26 @@ jobs:
key: v1-deps-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run:
name: Build
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
NX_BIN_URL=http://127.0.0.1:8080/assets/js NX_WASM_URL=http://127.0.0.1:8080/assets/js NPM_URL=http://localhost:9090/ yarn build:production
NX_BIN_URL=http://127.0.0.1:8080/assets/js/soljson NX_WASM_URL=http://127.0.0.1:8080/assets/js/soljson NPM_URL=http://localhost:9090/ yarn build:production
else
NX_BIN_URL=http://127.0.0.1:8080/assets/js NX_WASM_URL=http://127.0.0.1:8080/assets/js NPM_URL=http://localhost:9090/ yarn build
NX_BIN_URL=http://127.0.0.1:8080/assets/js/soljson NX_WASM_URL=http://127.0.0.1:8080/assets/js/soljson NPM_URL=http://localhost:9090/ yarn build
fi
- run: yarn run build:e2e
- run: grep -ir "[0-9]+commit" apps/* libs/* --include \*.ts --include \*.tsx --include \*.json > soljson-versions.txt
- restore_cache:
keys:
- soljson-v7-{{ checksum "soljson-versions.txt" }}
- run: yarn run downloadsolc_assets_e2e
- save_cache:
key: soljson-v7-{{ checksum "soljson-versions.txt" }}
paths:
- dist/apps/remix-ide/assets/js/soljson
- run: mkdir persist && zip -0 -r persist/dist.zip dist
- persist_to_workspace:
root: .
@ -43,7 +53,7 @@ jobs:
build-plugin:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -69,7 +79,7 @@ jobs:
lint:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -86,7 +96,7 @@ jobs:
command: node ./apps/remix-ide/ci/lint-targets.js
remix-libs:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -98,6 +108,7 @@ jobs:
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn --version
- run: yarn
- run: yarn build:libs
- run: cd dist/libs/remix-tests && yarn
@ -111,7 +122,7 @@ jobs:
remix-ide-browser:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -154,7 +165,7 @@ jobs:
at: .
- run: unzip ./persist/dist.zip
- run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules
- run: yarn run downloadsolc_assets_e2e && yarn run downloadsolc_assets_dist
- run: ls -la ./dist/apps/remix-ide/assets/js
- run: yarn run selenium-install || yarn run selenium-install
- run:
@ -175,7 +186,7 @@ jobs:
remix-test-plugins:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -201,7 +212,6 @@ jobs:
- run: unzip ./persist/dist.zip
- run: unzip ./persist/plugin-<< parameters.plugin >>.zip
- 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 selenium-install || yarn run selenium-install
- run:
name: Start Selenium
@ -216,7 +226,7 @@ jobs:
predeploy:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -231,7 +241,6 @@ jobs:
paths:
- node_modules
- run: yarn build:production
- run: yarn run downloadsolc_assets_dist
- run: mkdir persist && zip -0 -r persist/predeploy.zip dist
- persist_to_workspace:
root: .
@ -240,7 +249,7 @@ jobs:
deploy-build:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge

@ -11,11 +11,10 @@ jobs:
uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 14.17.6
node-version: 20.0.0
- run: yarn install
- run: ls
- run: pwd
- run: yarn run downloadsolc_assets
- run: yarn run build:production
- 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

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

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

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

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

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

@ -13,4 +13,4 @@ TODO
.tern-port
temp_publish_docker
src/assets/version.json
src/assets/js/soljson-v*
src/assets/js/soljson/soljson-v*

@ -1,63 +1,77 @@
const testFolder = './apps/remix-ide-e2e/src/tests/';
const fs = require('fs');
var child_process = require('child_process');
const { exit } = require('process');
let url = 'https://binaries.soliditylang.org/wasm/list.json'
const child = child_process.spawnSync('grep -r --include="*.json" --include="*.ts" --include="*.tsx" "+commit" apps/**/* libs/**/*', [], { encoding: 'utf8', cwd: process.cwd(), shell: true });
const axios = require('axios')
if (child.error) {
console.log("ERROR: ", child);
exit(1);
}
// use axios to download the file
axios({
url: url,
method: 'GET',
}).then((response) => {
let info = response.data;
info.builds = info.builds.filter(build => build.path.indexOf('nightly') === -1)
for (let build of info.builds) {
let soljson =[];
const buildurl = `https://solc-bin.ethereum.org/wasm/${build.path}`;
console.log(buildurl)
const quotedVersionsRegex = /['"v]\d*\.\d*\.\d*\+commit\.[\d\w]*/g;
let quotedVersionsRegexMatch = child.stdout.match(quotedVersionsRegex)
if(quotedVersionsRegexMatch){
let soljson2 = quotedVersionsRegexMatch.map((item) => item.replace('\'', 'v').replace('"', 'v'))
console.log('non nightly soljson versions found: ', soljson2);
if(soljson2) soljson = soljson.concat(soljson2);
}
const path = `./dist/apps/remix-ide/assets/js/${build.path}`;
// use axios to get the file
axios({
method: 'get',
url: buildurl,
responseType: 'stream'
}).then(function (response) {
// pipe the result stream into a file on disc
response.data.pipe(fs.createWriteStream(path));
})
}
const nightlyVersionsRegex = /\d*\.\d*\.\d-nightly.*\+commit\.[\d\w]*/g
const nightlyVersionsRegexMatch = child.stdout.match(nightlyVersionsRegex)
if(nightlyVersionsRegexMatch){
let soljson3 = nightlyVersionsRegexMatch.map((item) => 'v' + item);
console.log('nightly soljson versions found: ', soljson3);
if(soljson3) soljson = soljson.concat(soljson3);
}
)
fs.readdirSync(testFolder).forEach(file => {
let c = fs.readFileSync(testFolder + file, 'utf8');
const re = /(?<=soljson).*(?=(.js))/g;
const soljson = c.match(re);
if (soljson) {
for (let i = 0; i < soljson.length; i++) {
const version = soljson[i];
if (version && version.indexOf('nightly') > -1) {
const url = `https://solc-bin.ethereum.org/bin/soljson${version}.js`;
console.log(url)
const path = `./dist/apps/remix-ide/assets/js/soljson${version}.js`;
// use axios to get the file
axios({
method: 'get',
url: url,
responseType: 'stream'
}).then(function (response) {
// pipe the result stream into a file on disc
response.data.pipe(fs.createWriteStream(path));
})
if (soljson) {
// filter out duplicates
soljson = soljson.filter((item, index) => soljson.indexOf(item) === index);
// manually add some versions
soljson.push('v0.7.6+commit.7338295f');
console.log('soljson versions found: ', soljson, soljson.length);
for (let i = 0; i < soljson.length; i++) {
const version = soljson[i];
if (version) {
let url = ''
// if nightly
if (version.includes('nightly')) {
url = `https://binaries.soliditylang.org/bin/soljson-${version}.js`;
}else{
url = `https://binaries.soliditylang.org/wasm/soljson-${version}.js`;
}
const dir = './dist/apps/remix-ide/assets/js/soljson';
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
const path = `./dist/apps/remix-ide/assets/js/soljson/soljson-${version}.js`;
// check if the file exists
const exists = fs.existsSync(path);
if (!exists) {
console.log('URL:', url)
try {
// use curl to download the file
child_process.exec(`curl -o ${path} ${url}`, { encoding: 'utf8', cwd: process.cwd(), shell: true })
} catch (e) {
console.log('Failed to download soljson' + version + ' from ' + url)
}
}
}
}
}
}
});

@ -0,0 +1,33 @@
#!/usr/bin/env bash
echo "Downloading latest soljson.js from https://binaries.soliditylang.org/wasm/list.json"
set -e
# check if curl is installed
if ! command -v curl &> /dev/null
then
echo "curl could not be found"
exit
fi
# download https://binaries.soliditylang.org/wasm/list.json as json
curl -s https://binaries.soliditylang.org/wasm/list.json > list.json
# get the latest version without jq
latest=$(grep 'latestRelease' list.json | cut -d '"' -f 4)
echo "latest version: $latest"
# get url
url=$(grep "\"$latest\":" list.json | cut -d '"' -f 4)
echo "url: $url"
path="https://binaries.soliditylang.org/bin/$url"
echo "path: $path"
# download the file to ./apps/remix-ide/src/assets/js/soljson.js
curl -s $path > ./apps/remix-ide/src/assets/js/soljson.js
# if directory ./apps/remix-ide/src/assets/js/soljson does not exist, create it
if [ ! -d "./apps/remix-ide/src/assets/js/soljson" ]; then
mkdir ./apps/remix-ide/src/assets/js/soljson
fi
cp ./apps/remix-ide/src/assets/js/soljson.js ./apps/remix-ide/src/assets/js/soljson/$url
# remove list.json
rm list.json

@ -52,6 +52,7 @@
"configurations": {
"development": {
"buildTarget": "remix-ide:build:development",
"host": "0.0.0.0",
"port": 8080
},
"hot":{

@ -54,7 +54,7 @@
"filePanel.checkoutGitBranch": "Checkout Git Branch",
"filePanel.findOrCreateABranch": "Find or create a branch.",
"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.customizeTemplate": "Customize template",
"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.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.gitAccessTokenTitle": "GitHub Access Token",
"settings.gitAccessTokenText": "Manage the access token used to publish to Gist and retrieve GitHub contents.",
"settings.gitAccessTokenTitle": "Github Credentials",
"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.etherscanTokenTitle": "EtherScan Access Token",
"settings.etherscanAccessTokenText": "Manage the api key used to interact with Etherscan.",

@ -129,13 +129,16 @@ sub {
sup {
top:-.5em
}
i {
color: #074438;
}
a {
color:#18bc9c;
color:#074438;
text-decoration:none;
background-color:transparent
}
a:hover {
color:#0f7864;
color:#10947c;
text-decoration:underline
}
a:not([href]):not([class]) {
@ -182,8 +185,10 @@ th {
text-align:-webkit-match-parent
}
label {
display:inline-block;
margin-bottom:.5rem
display: inline-block;
margin-bottom: 0.5rem;
font-size: 11px;
line-height: 12px;
}
button {
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
}
.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 {
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 {
border-radius:.25rem
@ -7175,7 +7180,7 @@ a.text-dark:focus,a.text-dark:hover {
border:none; color:#fff
}
.alert .alert-link,.alert a {
color:#fff; text-decoration:underline
color:#18bc9c; text-decoration:underline
}
.alert-primary {
background-color:#2c3e50

@ -6,6 +6,7 @@ const version = require('../../package.json').version
const fs = require('fs')
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
const axios = require('axios')
const versionData = {
version: version,
@ -13,8 +14,17 @@ const versionData = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
}
const loadLocalSolJson = async () => {
// execute apps/remix-ide/ci/downloadsoljson.sh
const child = require('child_process').execSync('bash ./apps/remix-ide/ci/downloadsoljson.sh', { encoding: 'utf8', cwd: process.cwd(), shell: true })
// show output
console.log(child)
}
fs.writeFileSync('./apps/remix-ide/src/assets/version.json', JSON.stringify(versionData))
loadLocalSolJson()
const project = fs.readFileSync('./apps/remix-ide/project.json', 'utf8')
const implicitDependencies = JSON.parse(project).implicitDependencies
@ -120,3 +130,5 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
return config;
});

@ -40,7 +40,7 @@
"license": "MIT",
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-analyzer#readme",
"devDependencies": {
"@types/node": "^13.7.0",
"@types/node": "^18.16.1",
"babel-eslint": "^7.1.1",
"babel-plugin-transform-object-assign": "^6.22.0",
"babel-preset-es2015": "^6.24.0",

@ -9,7 +9,7 @@ import { BreakpointManager } from '../src/code/breakpointManager'
const compiler = require('solc')
const vmCall = require('./vmCall')
const ballot = `pragma solidity >=0.4.22 <0.8.0;
const ballot = `pragma solidity >=0.4.22;
/**
* @title Ballot
@ -287,7 +287,7 @@ function testDebugging (debugManager) {
breakPointManager.add({fileName: 'test.sol', row: 39})
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)
})

@ -95,7 +95,7 @@ tape('SourceLocationTracker', function (t) {
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['start'], 1293)
st.equal(map['start'], 1297)
st.equal(map['length'], 32)
map = await sourceLocationTracker.getValidSourceLocationFromVMTraceIndex('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', 36, output.contracts)

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

@ -38,7 +38,7 @@
"@babel/preset-es2015": "latest",
"@babel/preset-es2017": "latest",
"@babel/preset-stage-0": "^7.0.0",
"@types/node": "^13.1.1",
"@types/node": "^18.16.1",
"babel-eslint": "^10.0.0",
"babelify": "^10.0.0",
"typescript": "^3.7.4"

@ -58,6 +58,7 @@
"express-ws": "^4.0.0",
"merge": "^1.2.0",
"signale": "^1.4.0",
"solc": "^0.7.4",
"string-similarity": "^4.0.4",
"time-stamp": "^2.2.0",
"tslib": "^2.3.0",
@ -71,7 +72,7 @@
"@types/colors": "^1.2.1",
"@types/commander": "^2.12.2",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.21",
"@types/node": "^18.16.1",
"@types/web3": "^1.0.18",
"mocha": "^5.1.0",
"ts-node": "^8.0.2",

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

@ -946,7 +946,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
</div>}
>
<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"
>
<span>

@ -721,7 +721,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
tooltipId="generateTestsButtontooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='solidityUnitTesting.generateTestsButtonTooltip' />}
placement={'bottom-start'}
placement={'top'}
>
<button
className="btn border w-50"
@ -739,7 +739,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
tooltipId="generateTestsLinktooltip"
tooltipClasses="text-nowrap"
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">
<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 className="d-flex p-2">
<CustomTooltip
placement={'top-start'}
placement={'top'}
tooltipClasses="text-nowrap"
tooltipId="info-recorder"
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>
</button>
</CustomTooltip>
<button id="runTestsTabStopAction" data-id="testTabRunTestsTabStopAction" className="w-50 pl-2 ml-2 btn btn-secondary" disabled={disableStopButton} onClick={stopTests}>
<CustomTooltip
placement={'top-start'}
tooltipClasses="text-nowrap"
tooltipId="info-recorder"
tooltipText={<FormattedMessage id='solidityUnitTesting.runTestsTabStopActionTooltip' />}
>
<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>
</span>
</CustomTooltip>
</button>
<CustomTooltip
placement={'top'}
tooltipClasses="text-nowrap"
tooltipId="info-recorder"
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 className="fas fa-stop ml-2"></span>
<label className="labelOnBtn btn btn-secondary p-1 ml-2 m-0" id="runTestsTabStopActionLabel">{stopButtonLabel}</label>
</span>
</button>
</CustomTooltip>
</div>
<div className="d-flex align-items-center mx-3 pb-2 mt-2 border-bottom">
<input id="checkAllTests"

@ -97,7 +97,7 @@ export const createWorkspace = async (workspaceName: string, workspaceTemplateNa
if (!currentBranch) {
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 {
// commit the template as first 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 { action, FileExplorerContextMenuProps } from '../types'
import '../css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api'
import UploadFile from './upload-file'
declare global {
interface Window {
@ -13,9 +14,11 @@ declare global {
const _paq = window._paq = window._paq || [] //eslint-disable-line
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 intl = useIntl()
const [showFileExplorer, setShowFileExplorer] = useState(false)
useEffect(() => {
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
**/
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') })
if(focus[0].key === "contextMenu"){
return true
}
if (nonRootFocus.length > 1) {
for (const element of nonRootFocus) {
if (!itemMatchesCondition(item, element.type, element.key)) return false
@ -65,6 +73,29 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
const menu = () => {
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
id={`menuitem${item.name.toLowerCase()}`}
key={index}
@ -128,6 +159,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
deletePath(getPath())
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'deleteAll'])
break
case 'Publish Workspace to Gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace'])
publishFolderToGist(path, type)
break
default:
_paq.push(['trackEvent', 'fileExplorer', 'customAction', `${item.id}/${item.name}`])
emit && emit({ ...item, path: [path] } as customAction)
@ -137,7 +172,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
}}>{intl.formatMessage({id: `filePanel.${item.id}`, defaultMessage: item.label || item.name})}</li>
})
}
return (
<div
id="menuItemsContainer"
@ -148,6 +183,8 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
tabIndex={500}
{...otherProps}
>
{showFileExplorer && <UploadFile onUpload={(target)=> {
uploadFile(target); }} multiple />}
<ul id='remixui_menuitems'>{menu()}</ul>
</div>
)

@ -1,5 +1,5 @@
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 { Placement } from 'react-bootstrap/esm/Overlay'
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 { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, MenuItems, FileExplorerState } from '../types'
import { customAction } from '@remixproject/plugin-api'
import { contextMenuActions } from '../utils'
import { FileExplorerProps, WorkSpaceState } from '../types'
import '../css/file-explorer.css'
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'
export const FileExplorer = (props: FileExplorerProps) => {
const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props
const [state, setState] = useState<FileExplorerState>({
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: []
})
const [canPaste, setCanPaste] = useState(false)
const { name, contextMenuItems, removedContextMenuItems, files, workspaceState, toGist, addMenuItems,
removeMenuItems, handleContextMenu, handleNewFileInput, handleNewFolderInput, uploadFile, uploadFolder, fileState } = props
const [state, setState] = useState<WorkSpaceState>( workspaceState)
const treeRef = useRef<HTMLDivElement>(null)
useEffect(() => {
@ -59,6 +37,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}, [props.focusEdit])
useEffect(() => {
setState(workspaceState)
}, [workspaceState])
useEffect(() => {
if (treeRef.current) {
const keyPressHandler = (e: KeyboardEvent) => {
@ -87,62 +69,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}, [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 => {
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true
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) => {
try {
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) => {
try {
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) => {
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') => {
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) => {
if (typeof content === 'string') content = content.trim()
const parentFolder = extractParentFromKey(state.focusEdit.element)
@ -366,51 +199,7 @@ 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) => {
e.stopPropagation()
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerUploadFileuploadFile') return // we don't want to let propagate the input of type file
@ -425,17 +214,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
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) => {
try {
props.dispatchMoveFile(src, dest)
@ -454,76 +232,50 @@ export const FileExplorer = (props: FileExplorerProps) => {
return (
<Drag onFileMoved={handleFileMove} onFolderMoved={handleFolderMove}>
<div ref={treeRef} tabIndex={0} style={{ outline: "none" }}>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem"
controlBehaviour={true}
label={
<div onClick={handleFileExplorerMenuClick}>
<FileExplorerMenu
title={''}
menuItems={props.menuItems}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
publishToGist={publishToGist}
uploadFile={uploadFile}
uploadFolder={uploadFolder}
/>
</div>
}
expand={true}>
<div className='pb-4 mb-4'>
<TreeView id='treeViewMenu'>
{
files[ROOT_PATH] && Object.keys(files[ROOT_PATH]).map((key, index) => <FileRender
file={files[ROOT_PATH][key]}
fileDecorations={fileState}
index={index}
focusContext={state.focusContext}
focusEdit={state.focusEdit}
focusElement={props.focusElement}
ctrlKey={state.ctrlKey}
expandPath={props.expandPath}
editModeOff={editModeOff}
handleClickFile={handleClickFile}
handleClickFolder={handleClickFolder}
handleContextMenu={handleContextMenu}
key={index}
showIconsMenu={props.showIconsMenu}
hideIconsMenu={props.hideIconsMenu}
/>)
}
</TreeView>
<div ref={treeRef} tabIndex={0} style={{ outline: "none" }}>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem"
controlBehaviour={true}
label={
<div onClick={handleFileExplorerMenuClick}>
<FileExplorerMenu
title={''}
menuItems={props.menuItems}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
publishToGist={publishToGist}
uploadFile={uploadFile}
uploadFolder={uploadFolder}
/>
</div>
</TreeViewItem>
</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>
}
expand={true}>
<div className='pb-4 mb-4'>
<TreeView id='treeViewMenu'>
{
files[ROOT_PATH] && Object.keys(files[ROOT_PATH]).map((key, index) => <FileRender
file={files[ROOT_PATH][key]}
fileDecorations={fileState}
index={index}
focusContext={state.focusContext}
focusEdit={state.focusEdit}
focusElement={props.focusElement}
ctrlKey={state.ctrlKey}
expandPath={props.expandPath}
editModeOff={editModeOff}
handleClickFile={handleClickFile}
handleClickFolder={handleClickFolder}
handleContextMenu={handleContextMenu}
key={index}
showIconsMenu={props.showIconsMenu}
hideIconsMenu={props.hideIconsMenu}
/>)
}
</TreeView>
</div>
</TreeViewItem>
</TreeView>
</div>
</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 { 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 { FileSystemContext } from './contexts'
import './css/remix-ui-workspace.css'
import { ROOT_PATH, TEMPLATE_NAMES } from './utils/constants'
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 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 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(() => {
let workspaceName = localStorage.getItem('currentWorkspace')
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 = () => {
global.modal(
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) => {
setShowDropdown(isOpen)
}
@ -428,8 +669,12 @@ export function Workspace () {
return (
<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='d-flex flex-column w-100 mb-1 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }} onContextMenu={(e)=>{
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>
<header>
<div className="mx-2 mb-2 d-flex flex-column">
@ -455,7 +700,7 @@ export function Workspace () {
createWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate'])
}}
style={{ fontSize: 'large' }}
style={{ fontSize: 'medium' }}
className='far fa-plus remixui_menuicon d-flex align-self-end'
>
</span>
@ -537,19 +782,20 @@ export function Workspace () {
</div>
</header>
</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'>
{ (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.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'>
<FileExplorer
fileState={global.fs.browser.fileState}
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
fileState={global.fs.browser.fileState}
workspaceState={state}
expandPath={global.fs.browser.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
@ -578,12 +824,24 @@ export function Workspace () {
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
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>
}
{ 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) &&
<div className='h-100 filesystemexplorer pb-4 mb-4 remixui_treeview'>
<div className='h-100 filesystemexplorer remixui_treeview'>
<FileExplorer
name='localhost'
menuItems={['createNewFile', 'createNewFolder']}
@ -591,6 +849,7 @@ export function Workspace () {
removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
files={global.fs.localhost.files}
fileState={[]}
workspaceState={state}
expandPath={global.fs.localhost.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
@ -619,6 +878,18 @@ export function Workspace () {
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
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>
}
@ -685,8 +956,34 @@ export function Workspace () {
</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>
)
}
export default Workspace
export default Workspace

@ -5,7 +5,7 @@ import { fileDecoration } from '@remix-ui/file-decorators'
import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types'
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 {
language: "Solidity";
settings?: any,
@ -82,6 +82,7 @@ export interface FileExplorerProps {
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
files: { [x: string]: Record<string, FileType> },
workspaceState: WorkSpaceState,
fileState: fileDecoration[],
expandPath: string[],
focusEdit: string,
@ -111,7 +112,19 @@ export interface FileExplorerProps {
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: 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
export interface FileExplorerMenuProps {
@ -149,27 +162,23 @@ export interface FileExplorerContextMenuProps {
copyFileName?: (path: string, type: string) => void
copyPath?: (path: string, type: string) => void
generateUml?: (path: string) => Promise<void>
uploadFile?: (target: EventTarget & HTMLInputElement) => void
}
export interface FileExplorerState {
export interface WorkSpaceState {
ctrlKey: boolean
newFileName: string
actions: {
id: string
name: string
type?: Array<'folder' | 'gist' | 'file'>
type?: Array<'folder' | 'gist' | 'file' | 'workspace'>
path?: string[]
extension?: string[]
pattern?: string[]
multiselect: boolean
label: string
}[]
focusContext: {
element: string
x: number
y: number
type: string
}
focusContext: FileFocusContextType
focusEdit: {
element: string
type: string
@ -179,8 +188,17 @@ export interface FileExplorerState {
mouseOverElement: string
showContextMenu: boolean
reservedKeywords: string[]
copyElement: {
key: string
type: 'folder' | 'gist' | 'file'
}[]
copyElement: CopyElementType[]
}
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 = [{
id: 'newFile',
name: 'New File',
type: ['folder', 'gist'],
type: ['folder', 'gist', 'workspace'],
multiselect: false,
label: ''
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder', 'gist'],
type: ['folder', 'gist', 'workspace'],
multiselect: false,
label: ''
}, {
@ -27,7 +27,7 @@ export const contextMenuActions: MenuItems = [{
},{
id: 'download',
name: 'Download',
type: ['file', 'folder'],
type: ['file', 'folder', 'workspace'],
multiselect: false,
label: ''
}, {
@ -78,4 +78,23 @@ export const contextMenuActions: MenuItems = [{
type: ['folder', 'file'],
multiselect: true,
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: ''
}]

@ -31,7 +31,7 @@
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.18",
"@types/node": "^18.16.1",
"chai": "^4.2.0",
"mocha": "^5.1.0",
"remix-plugin": "0.0.1-alpha.2",

@ -49,7 +49,7 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/fs-extra": "^9.0.1",
"@types/node": "^14.0.5",
"@types/node": "^18.16.1",
"@types/ws": "^7.2.4",
"@typescript-eslint/eslint-plugin": "^3.2.0",
"@typescript-eslint/parser": "^3.2.0",

@ -21,7 +21,7 @@
"remix-ide": "./apps/remix-ide/bin/remix-ide"
},
"engines": {
"node": "^14.17.6",
"node": "^20.0.0",
"npm": "^6.14.15"
},
"scripts": {
@ -48,7 +48,7 @@
"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",
"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",
"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",
@ -58,8 +58,6 @@
"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/",
"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",
"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",
@ -200,7 +198,6 @@
"sol2uml": "^2.4.3",
"string-similarity": "^4.0.4",
"svg2pdf.js": "^2.2.1",
"swarmgw": "^0.3.1",
"time-stamp": "^2.2.0",
"toml": "^3.0.0",
"tree-kill": "^1.2.2",
@ -220,8 +217,6 @@
"@babel/plugin-transform-runtime": "^7.10.4",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.19.4",
"@babel/preset-es2015": "^7.0.0-beta.53",
"@babel/preset-es2017": "latest",
"@babel/preset-react": "^7.18.6",
"@babel/preset-stage-0": "^7.0.0",
"@babel/preset-typescript": "^7.18.6",
@ -249,7 +244,7 @@
"@types/isomorphic-git__lightning-fs": "^4.4.2",
"@types/lodash": "^4.14.172",
"@types/mocha": "^9.1.1",
"@types/node": "18.7.18",
"@types/node": "18.16.1",
"@types/react": "^17.0.24",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9",
@ -312,7 +307,7 @@
"ganache-cli": "^6.8.1",
"gists": "^1.0.1",
"gulp": "^4.0.2",
"hardhat": "^2.12.7",
"hardhat": "^2.14.0",
"https-browserify": "^1.0.0",
"ipfs-http-client": "^47.0.1",
"ipfs-mini": "^1.1.5",
@ -343,7 +338,7 @@
"rimraf": "^2.6.1",
"selenium-standalone": "^8.2.3",
"semver": "^7.4.0",
"solc": "0.7.4",
"solc": "^0.7.4",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"style-loader": "^3.3.1",

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