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. 46
      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. 32
      libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
  21. 2
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  22. 43
      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. 352
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  25. 31
      libs/remix-ui/workspace/src/lib/components/upload-file.tsx
  26. 315
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  27. 46
      libs/remix-ui/workspace/src/lib/types/index.ts
  28. 29
      libs/remix-ui/workspace/src/lib/utils/index.ts
  29. 11
      package.json
  30. 661
      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
@ -43,7 +43,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 +69,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 +86,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 +98,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 +112,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 +155,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: yarn run downloadsolc_assets_e2e
- run: ls -la ./dist/apps/remix-ide/assets/js
- run: yarn run selenium-install || yarn run selenium-install
- run:
@ -175,7 +176,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 +202,7 @@ 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 downloadsolc_assets_e2e
- run: yarn run selenium-install || yarn run selenium-install
- run:
name: Start Selenium
@ -216,7 +217,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 +232,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 +240,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

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

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

@ -20,14 +20,20 @@ axios({
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));
})
try {
axios({
method: 'get',
url: buildurl,
}).then(function (response) {
fs.writeFile(path, response.data, function (err) {
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`;
// 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));
})
try {
axios({
method: 'get',
url: url,
}).then(function (response) {
fs.writeFile(path, response.data, function (err) {
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.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,33 @@ const versionData = {
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))
loadLocalSolJson()
const project = fs.readFileSync('./apps/remix-ide/project.json', 'utf8')
const implicitDependencies = JSON.parse(project).implicitDependencies
@ -120,3 +146,5 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
return config;
});

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

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

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

@ -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",
@ -310,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",
@ -341,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