Merge branch 'master' into le

pull/5370/head
Liana Husikyan 4 months ago committed by GitHub
commit 4728302896
  1. 17
      apps/circuit-compiler/src/app/services/circomPluginClient.ts
  2. 5
      apps/remix-ide-e2e/src/commands/selectFiles.ts
  3. 77
      apps/remix-ide-e2e/src/tests/file_explorer_multiselect.test.ts
  4. 5
      apps/remix-ide/ci/deploy_from_travis_remix-alpha.sh
  5. 5
      apps/remix-ide/ci/deploy_from_travis_remix-beta.sh
  6. 5
      apps/remix-ide/ci/deploy_from_travis_remix-live.sh
  7. 43
      apps/remix-ide/ci/gh-actions-deploy.yml
  8. 12
      apps/remix-ide/src/app/files/fileManager.ts
  9. 1
      apps/remix-ide/src/app/plugins/git.tsx
  10. 1
      apps/remix-ide/src/app/tabs/locales/en/terminal.json
  11. 1
      apps/remix-ide/src/app/tabs/locales/es/terminal.json
  12. 3
      apps/remix-ide/src/app/tabs/locales/fr/terminal.json
  13. 1
      apps/remix-ide/src/app/tabs/locales/it/terminal.json
  14. 1
      apps/remix-ide/src/app/tabs/locales/ko/terminal.json
  15. 1
      apps/remix-ide/src/app/tabs/locales/ru/terminal.json
  16. 1
      apps/remix-ide/src/app/tabs/locales/zh/terminal.json
  17. 2
      libs/remix-lib/package.json
  18. 1
      libs/remix-ui/app/src/lib/remix-app/style/remix-app.css
  19. 13
      libs/remix-ui/terminal/src/lib/components/Table.tsx
  20. 18
      libs/remix-ui/workspace/src/lib/actions/index.ts
  21. 170
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  22. 61
      libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx
  23. 48
      libs/remix-ui/workspace/src/lib/components/flat-tree.tsx
  24. 2
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  25. 4
      libs/remix-ui/workspace/src/lib/css/file-explorer.css
  26. 14
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  27. 4
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  28. 31
      libs/remix-ui/workspace/src/lib/types/index.ts
  29. 33
      libs/remix-ui/workspace/src/lib/utils/getEventTarget.ts

@ -10,8 +10,6 @@ import * as compilerV215 from 'circom_wasm/v2.1.5'
import { extractNameFromKey, extractParentFromKey } from '@remix-ui/helper' import { extractNameFromKey, extractParentFromKey } from '@remix-ui/helper'
import { CompilationConfig, CompilerReport, PrimeValue, ResolverOutput } from '../types' import { CompilationConfig, CompilerReport, PrimeValue, ResolverOutput } from '../types'
// @ts-ignore
const _paq = (window._paq = window._paq || [])
export class CircomPluginClient extends PluginClient { export class CircomPluginClient extends PluginClient {
public internalEvents: EventManager public internalEvents: EventManager
private _compilationConfig: CompilationConfig = { private _compilationConfig: CompilationConfig = {
@ -22,6 +20,11 @@ export class CircomPluginClient extends PluginClient {
private lastParsedFiles: Record<string, string> = {} private lastParsedFiles: Record<string, string> = {}
private lastCompiledFile: string = '' private lastCompiledFile: string = ''
private compiler: typeof compilerV215 & typeof compilerV216 & typeof compilerV217 & typeof compilerV218 private compiler: typeof compilerV215 & typeof compilerV216 & typeof compilerV217 & typeof compilerV218
private _paq = {
push: (args) => {
this.call('matomo' as any, 'track', args)
}
}
constructor() { constructor() {
super() super()
@ -164,7 +167,7 @@ export class CircomPluginClient extends PluginClient {
const circuitErrors = circuitApi.report() const circuitErrors = circuitApi.report()
this.logCompilerReport(circuitErrors) this.logCompilerReport(circuitErrors)
_paq.push(['trackEvent', 'circuit-compiler', 'compile', 'Compilation failed']) this._paq.push(['trackEvent', 'circuit-compiler', 'compile', 'Compilation failed'])
throw new Error(circuitErrors) throw new Error(circuitErrors)
} else { } else {
this.lastCompiledFile = path this.lastCompiledFile = path
@ -184,7 +187,7 @@ export class CircomPluginClient extends PluginClient {
} else { } else {
this.internalEvents.emit('circuit_compiling_done', []) this.internalEvents.emit('circuit_compiling_done', [])
} }
_paq.push(['trackEvent', 'circuit-compiler', 'compile', 'Compilation successful']) this._paq.push(['trackEvent', 'circuit-compiler', 'compile', 'Compilation successful'])
circuitApi.log().map(log => { circuitApi.log().map(log => {
log && this.call('terminal', 'log', { type: 'log', value: log }) log && this.call('terminal', 'log', { type: 'log', value: log })
}) })
@ -226,7 +229,7 @@ export class CircomPluginClient extends PluginClient {
const r1csErrors = r1csApi.report() const r1csErrors = r1csApi.report()
this.logCompilerReport(r1csErrors) this.logCompilerReport(r1csErrors)
_paq.push(['trackEvent', 'circuit-compiler', 'generateR1cs', 'R1CS Generation failed']) this._paq.push(['trackEvent', 'circuit-compiler', 'generateR1cs', 'R1CS Generation failed'])
throw new Error(r1csErrors) throw new Error(r1csErrors)
} else { } else {
this.internalEvents.emit('circuit_generating_r1cs_done') this.internalEvents.emit('circuit_generating_r1cs_done')
@ -235,7 +238,7 @@ export class CircomPluginClient extends PluginClient {
// @ts-ignore // @ts-ignore
await this.call('fileManager', 'writeFile', writePath, r1csProgram, true) await this.call('fileManager', 'writeFile', writePath, r1csProgram, true)
_paq.push(['trackEvent', 'circuit-compiler', 'generateR1cs', 'R1CS Generation successful']) this._paq.push(['trackEvent', 'circuit-compiler', 'generateR1cs', 'R1CS Generation successful'])
r1csApi.log().map(log => { r1csApi.log().map(log => {
log && this.call('terminal', 'log', { type: 'log', value: log }) log && this.call('terminal', 'log', { type: 'log', value: log })
}) })
@ -256,7 +259,7 @@ export class CircomPluginClient extends PluginClient {
const witness = this.compiler ? await this.compiler.generate_witness(dataRead, input) : await generate_witness(dataRead, input) const witness = this.compiler ? await this.compiler.generate_witness(dataRead, input) : await generate_witness(dataRead, input)
// @ts-ignore // @ts-ignore
await this.call('fileManager', 'writeFile', wasmPath.replace('.wasm', '.wtn'), witness, true) await this.call('fileManager', 'writeFile', wasmPath.replace('.wasm', '.wtn'), witness, true)
_paq.push(['trackEvent', 'circuit-compiler', 'computeWitness', 'Witness computing successful']) this._paq.push(['trackEvent', 'circuit-compiler', 'computeWitness', 'Witness computing successful'])
this.internalEvents.emit('circuit_computing_witness_done') this.internalEvents.emit('circuit_computing_witness_done')
this.emit('statusChanged', { key: 'succeed', title: 'witness computed successfully', type: 'success' }) this.emit('statusChanged', { key: 'succeed', title: 'witness computed successfully', type: 'success' })
} }

@ -8,15 +8,14 @@ class SelectFiles extends EventEmitter {
browser.perform(function () { browser.perform(function () {
const actions = this.actions({ async: true }) const actions = this.actions({ async: true })
actions.keyDown(this.Keys.SHIFT) actions.keyDown(this.Keys.SHIFT)
for(let i = 0; i < selectedElements.length; i++) { for (let i = 0; i < selectedElements.length; i++) {
actions.click(selectedElements[i].value) actions.click(selectedElements[i].value)
} }
return actions.contextClick(selectedElements[0].value) return actions//.contextClick(selectedElements[0].value)
}) })
this.emit('complete') this.emit('complete')
return this return this
} }
} }
module.exports = SelectFiles module.exports = SelectFiles

@ -2,6 +2,7 @@ import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init' import init from '../helpers/init'
module.exports = { module.exports = {
"@disabled": true,
before: function (browser: NightwatchBrowser, done: VoidFunction) { before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done) init(browser, done)
}, },
@ -10,11 +11,11 @@ module.exports = {
const selectedElements = [] const selectedElements = []
browser browser
.openFile('contracts') .openFile('contracts')
.click({ selector: '//*[@data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]', locateStrategy: 'xpath' }) .click({ selector: '//*[@data-id="treeViewDivtreeViewItemcontracts/1_Storage.sol"]', locateStrategy: 'xpath' })
.findElement({ selector: '//*[@data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]', locateStrategy: 'xpath' }, (el) => { .findElement({ selector: '//*[@data-id="treeViewDivtreeViewItemcontracts/2_Owner.sol"]', locateStrategy: 'xpath' }, (el) => {
selectedElements.push(el) selectedElements.push(el)
}) })
browser.findElement({ selector: '//*[@data-id="treeViewLitreeViewItemtests"]', locateStrategy: 'xpath' }, browser.findElement({ selector: '//*[@data-id="treeViewDivtreeViewItemtests"]', locateStrategy: 'xpath' },
(el: any) => { (el: any) => {
selectedElements.push(el) selectedElements.push(el)
}) })
@ -22,6 +23,74 @@ module.exports = {
.assert.visible('.bg-secondary[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]') .assert.visible('.bg-secondary[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.assert.visible('.bg-secondary[data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]') .assert.visible('.bg-secondary[data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]')
.assert.visible('.bg-secondary[data-id="treeViewLitreeViewItemtests"]') .assert.visible('.bg-secondary[data-id="treeViewLitreeViewItemtests"]')
.end() },
'Should drag and drop multiple files in file explorer to tests folder #group1': function (browser: NightwatchBrowser) {
const selectedElements = []
if (browser.options.desiredCapabilities?.browserName === 'firefox') {
console.log('Skipping test for firefox')
browser.end()
return;
} else {
browser
.click({ selector: '//*[@data-id="treeViewUltreeViewMenu"]', locateStrategy: 'xpath' })
.click({ selector: '//*[@data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]', locateStrategy: 'xpath' })
.findElement({ selector: '//*[@data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]', locateStrategy: 'xpath' }, (el) => {
selectedElements.push(el)
})
browser.selectFiles(selectedElements)
.perform((done) => {
browser.findElement({ selector: '//*[@data-id="treeViewLitreeViewItemtests"]', locateStrategy: 'xpath' },
(el: any) => {
const id = (el as any).value.getId()
browser
.waitForElementVisible('li[data-id="treeViewLitreeViewItemtests"]')
.dragAndDrop('li[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]', id)
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('li[data-id="treeViewLitreeViewItemtests/1_Storage.sol"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemtests/2_Owner.sol"]')
.waitForElementNotPresent('li[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.waitForElementNotPresent('li[data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]')
.perform(() => done())
})
})
}
},
'should drag and drop multiple files and folders in file explorer to contracts folder #group3': function (browser: NightwatchBrowser) {
const selectedElements = []
if (browser.options.desiredCapabilities?.browserName === 'firefox') {
console.log('Skipping test for firefox')
browser.end()
return;
} else {
browser
.clickLaunchIcon('filePanel')
.click({ selector: '//*[@data-id="treeViewLitreeViewItemtests"]', locateStrategy: 'xpath' })
.findElement({ selector: '//*[@data-id="treeViewDivtreeViewItemscripts"]', locateStrategy: 'xpath' }, (el) => {
selectedElements.push(el)
})
browser.findElement({ selector: '//*[@data-id="treeViewDivtreeViewItemREADME.txt"]', locateStrategy: 'xpath' },
(el: any) => {
selectedElements.push(el)
})
browser.selectFiles(selectedElements)
.perform((done) => {
browser.findElement({ selector: '//*[@data-id="treeViewLitreeViewItemcontracts"]', locateStrategy: 'xpath' },
(el: any) => {
const id = (el as any).value.getId()
browser
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts"]')
.dragAndDrop('li[data-id="treeViewLitreeViewItemtests"]', id)
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/tests"]', 5000)
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/README.txt"]', 5000)
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/scripts"]', 5000)
.waitForElementNotPresent('li[data-id="treeViewLitreeViewItemtests"]')
.waitForElementNotPresent('li[data-id="treeViewLitreeViewItemREADME.txt"]')
.perform(() => done())
})
})
}
} }
} }

@ -5,6 +5,11 @@ SHA=`git rev-parse --short --verify HEAD`
cd dist/apps/remix-ide cd dist/apps/remix-ide
# this gh action is used to deploy the build to the gh pages
mkdir dist/apps/remix-ide/.github
mkdir dist/apps/remix-ide/.github/workflows
cp apps/remix-ide/ci/gh-actions-deploy.yml dist/apps/remix-ide/.github/workflows
git init git init
git checkout -b gh-pages git checkout -b gh-pages
git config user.name "$COMMIT_AUTHOR" git config user.name "$COMMIT_AUTHOR"

@ -5,6 +5,11 @@ SHA=`git rev-parse --short --verify HEAD`
cd dist/apps/remix-ide cd dist/apps/remix-ide
# this gh action is used to deploy the build to the gh pages
mkdir dist/apps/remix-ide/.github
mkdir dist/apps/remix-ide/.github/workflows
cp apps/remix-ide/ci/gh-actions-deploy.yml dist/apps/remix-ide/.github/workflows
git init git init
git checkout -b gh-pages git checkout -b gh-pages
git config user.name "$COMMIT_AUTHOR" git config user.name "$COMMIT_AUTHOR"

@ -5,6 +5,11 @@ SHA=`git rev-parse --short --verify HEAD`
cd dist/apps/remix-ide cd dist/apps/remix-ide
# this gh action is used to deploy the build to the gh pages
mkdir dist/apps/remix-ide/.github
mkdir dist/apps/remix-ide/.github/workflows
cp apps/remix-ide/ci/gh-actions-deploy.yml dist/apps/remix-ide/.github/workflows/gh-actions-deploy.yml
git init git init
git checkout -b gh-pages git checkout -b gh-pages
git config user.name "$COMMIT_AUTHOR" git config user.name "$COMMIT_AUTHOR"

@ -0,0 +1,43 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["gh-pages"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: '.'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

@ -962,6 +962,12 @@ class FileManager extends Plugin {
return exists return exists
} }
/**
* Check if a file can be moved
* @param src source file
* @param dest destination file
* @returns {boolean} true if the file is allowed to be moved
*/
async moveFileIsAllowed (src: string, dest: string) { async moveFileIsAllowed (src: string, dest: string) {
try { try {
src = this.normalize(src) src = this.normalize(src)
@ -984,6 +990,12 @@ class FileManager extends Plugin {
} }
} }
/**
* Check if a folder can be moved
* @param src source folder
* @param dest destination folder
* @returns {boolean} true if the folder is allowed to be moved
*/
async moveDirIsAllowed (src: string, dest: string) { async moveDirIsAllowed (src: string, dest: string) {
try { try {
src = this.normalize(src) src = this.normalize(src)

@ -6,6 +6,7 @@ import * as packageJson from '../../../../../package.json'
const profile = { const profile = {
name: 'dgit', name: 'dgit',
displayName: 'Git',
desciption: 'Git plugin for Remix', desciption: 'Git plugin for Remix',
methods: ['pull', 'track', 'diff', 'clone', 'open'], methods: ['pull', 'track', 'diff', 'clone', 'open'],
events: [''], events: [''],

@ -40,5 +40,6 @@
"terminal.input": "input", "terminal.input": "input",
"terminal.decodedInput": "decoded input", "terminal.decodedInput": "decoded input",
"terminal.decodedOutput": "decoded output", "terminal.decodedOutput": "decoded output",
"terminal.rawlogs": "raw logs",
"terminal.logs": "logs" "terminal.logs": "logs"
} }

@ -39,5 +39,6 @@
"terminal.input": "entrada", "terminal.input": "entrada",
"terminal.decodedInput": "entrada descodificada", "terminal.decodedInput": "entrada descodificada",
"terminal.decodedOutput": "salida descodificada", "terminal.decodedOutput": "salida descodificada",
"terminal.rawlogs": "registros sin procesar",
"terminal.logs": "registros" "terminal.logs": "registros"
} }

@ -39,5 +39,6 @@
"terminal.input": "entrée", "terminal.input": "entrée",
"terminal.decodedInput": "entrée décodée", "terminal.decodedInput": "entrée décodée",
"terminal.decodedOutput": "sortie décodée", "terminal.decodedOutput": "sortie décodée",
"terminal.logs": "logs" "terminal.rawlogs": "logs bruts",
"terminal.logs": "Log"
} }

@ -39,5 +39,6 @@
"terminal.input": "input", "terminal.input": "input",
"terminal.decodedInput": "input decodificato", "terminal.decodedInput": "input decodificato",
"terminal.decodedOutput": "output decodificato", "terminal.decodedOutput": "output decodificato",
"terminal.rawlogs": "log grezzi",
"terminal.logs": "Log" "terminal.logs": "Log"
} }

@ -39,5 +39,6 @@
"terminal.input": "입력", "terminal.input": "입력",
"terminal.decodedInput": "디코드된 입력", "terminal.decodedInput": "디코드된 입력",
"terminal.decodedOutput": "디코드된 출력", "terminal.decodedOutput": "디코드된 출력",
"terminal.rawlogs": "원시 로그",
"terminal.logs": "로그" "terminal.logs": "로그"
} }

@ -39,5 +39,6 @@
"terminal.input": "ввод", "terminal.input": "ввод",
"terminal.decodedInput": "декодированный ввод", "terminal.decodedInput": "декодированный ввод",
"terminal.decodedOutput": "декодированный вывод", "terminal.decodedOutput": "декодированный вывод",
"terminal.rawlogs": "необработанные журналы",
"terminal.logs": "журналы" "terminal.logs": "журналы"
} }

@ -39,5 +39,6 @@
"terminal.input": "输入", "terminal.input": "输入",
"terminal.decodedInput": "解码输入", "terminal.decodedInput": "解码输入",
"terminal.decodedOutput": "解码输出", "terminal.decodedOutput": "解码输出",
"terminal.rawlogs": "原始日志",
"terminal.logs": "日志" "terminal.logs": "日志"
} }

@ -57,4 +57,4 @@
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "35e1469e94bb370f5427d4ab230fcbd47c665e55", "gitHead": "35e1469e94bb370f5427d4ab230fcbd47c665e55",
"types": "./src/index.d.ts" "types": "./src/index.d.ts"
} }

@ -43,6 +43,7 @@ pre {
.pinnedpanel { .pinnedpanel {
width : 320px; width : 320px;
transition : width 0.25s; transition : width 0.25s;
padding-bottom : 1.4rem;
} }
.highlightcode { .highlightcode {
position : absolute; position : absolute;

@ -188,7 +188,18 @@ const showTable = (opts, showTableHash) => {
<td className="remix_ui_terminal_td" data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}> <td className="remix_ui_terminal_td" data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>
{JSON.stringify(stringified, null, '\t')} {JSON.stringify(stringified, null, '\t')}
<CopyToClipboard content={JSON.stringify(stringified, null, '\t')} /> <CopyToClipboard content={JSON.stringify(stringified, null, '\t')} />
<CopyToClipboard content={JSON.stringify(opts.logs.raw || '0')} /> <CopyToClipboard content={JSON.stringify(opts.logs.raw || '0', null, 2)} />
</td>
</tr>
) : null}
{opts.logs ? (
<tr className="remix_ui_terminal_tr">
<td className="remix_ui_terminal_td" data-shared={`key_${opts.hash}`}>
<FormattedMessage id="terminal.rawlogs" />
</td>
<td className="remix_ui_terminal_td" data-id={`txLoggerTableHash${opts.hash}`} data-shared={`pair_${opts.hash}`}>
<pre>{JSON.stringify(opts.logs.raw || '0', null, 2)}</pre>
<CopyToClipboard content={JSON.stringify(opts.logs.raw || '0', null, 2)} />
</td> </td>
</tr> </tr>
) : null} ) : null}

@ -656,3 +656,21 @@ export const moveFolderIsAllowed = async (src: string, dest: string) => {
return isAllowed return isAllowed
} }
export const moveFilesIsAllowed = async (src: string[], dest: string) => {
const fileManager = plugin.fileManager
const boolArray: boolean[] = []
for (const srcFile of src) {
boolArray.push(await fileManager.moveFileIsAllowed(srcFile, dest))
}
return boolArray.every(p => p === true) || false
}
export const moveFoldersIsAllowed = async (src: string[], dest: string) => {
const fileManager = plugin.fileManager
const boolArray: boolean[] = []
for (const srcFile of src) {
boolArray.push(await fileManager.moveDirIsAllowed(srcFile, dest))
}
return boolArray.every(p => p === true) || false
}

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef, SyntheticEvent } from 'react' // eslint-disable-line import React, { useEffect, useState, useRef, SyntheticEvent, useContext } from 'react' // eslint-disable-line
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import { TreeView } from '@remix-ui/tree-view' // eslint-disable-line import { TreeView } from '@remix-ui/tree-view' // eslint-disable-line
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
@ -9,8 +9,9 @@ import '../css/file-explorer.css'
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, getPathIcon, joinPath } from '@remix-ui/helper' import { checkSpecialChars, extractNameFromKey, extractParentFromKey, getPathIcon, joinPath } from '@remix-ui/helper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ROOT_PATH } from '../utils/constants' import { ROOT_PATH } from '../utils/constants'
import { moveFileIsAllowed, moveFolderIsAllowed } from '../actions' import { moveFileIsAllowed, moveFilesIsAllowed, moveFolderIsAllowed, moveFoldersIsAllowed } from '../actions'
import { FlatTree } from './flat-tree' import { FlatTree } from './flat-tree'
import { FileSystemContext } from '../contexts'
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const intl = useIntl() const intl = useIntl()
@ -36,6 +37,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
// const [isPending, startTransition] = useTransition(); // const [isPending, startTransition] = useTransition();
const treeRef = useRef<HTMLDivElement>(null) const treeRef = useRef<HTMLDivElement>(null)
const { plugin } = useContext(FileSystemContext)
const [feTarget, setFeTarget] = useState<{ key: string, type: 'file' | 'folder' }[]>({} as { key: string, type: 'file' | 'folder' }[])
const [filesSelected, setFilesSelected] = useState<string[]>([])
const feWindow = (window as any)
useEffect(() => { useEffect(() => {
if (contextMenuItems) { if (contextMenuItems) {
addMenuItems(contextMenuItems) addMenuItems(contextMenuItems)
@ -96,6 +102,100 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, [treeRef.current]) }, [treeRef.current])
useEffect(() => {
const performDeletion = async () => {
const path: string[] = []
if (feTarget?.length > 0 && feTarget[0]?.key.length > 0) {
feTarget.forEach((one) => {
path.push(one.key)
})
await deletePath(path)
}
}
if (treeRef.current) {
const deleteKeyPressHandler = async (eve: KeyboardEvent) => {
if (eve.key === 'Delete' ) {
feWindow._paq.push(['trackEvent', 'fileExplorer', 'deleteKey', 'deletePath'])
setState((prevState) => {
return { ...prevState, deleteKey: true }
})
performDeletion()
return
}
if (eve.metaKey) {
if (eve.key === 'Backspace') {
feWindow._paq.push(['trackEvent', 'fileExplorer', 'osxDeleteKey', 'deletePath'])
setState((prevState) => {
return { ...prevState, deleteKey: true }
})
performDeletion()
return
}
}
}
const deleteKeyPressUpHandler = async (eve: KeyboardEvent) => {
if (eve.key === 'Delete' ) {
setState((prevState) => {
return { ...prevState, deleteKey: false }
})
return
}
if (eve.metaKey) {
if (eve.key === 'Backspace') {
setState((prevState) => {
return { ...prevState, deleteKey: false }
})
return
}
}
}
treeRef.current?.addEventListener('keydown', deleteKeyPressHandler)
treeRef.current?.addEventListener('keyup', deleteKeyPressUpHandler)
return () => {
treeRef.current?.removeEventListener('keydown', deleteKeyPressHandler)
treeRef.current?.removeEventListener('keyup', deleteKeyPressUpHandler)
}
}
}, [treeRef.current, feTarget])
useEffect(() => {
const performRename = async () => {
if (feTarget?.length > 1 && feTarget[0]?.key.length > 1) {
await plugin.call('notification', 'alert', { id: 'renameAlert', message: 'You cannot rename multiple files at once!' })
}
props.editModeOn(feTarget[0].key, feTarget[0].type, false)
}
if (treeRef.current) {
const F2KeyPressHandler = async (eve: KeyboardEvent) => {
if (eve.key === 'F2' ) {
feWindow._paq.push(['trackEvent', 'fileExplorer', 'f2ToRename', 'RenamePath'])
await performRename()
setState((prevState) => {
return { ...prevState, F2Key: true }
})
return
}
}
const F2KeyPressUpHandler = async (eve: KeyboardEvent) => {
if (eve.key === 'F2' ) {
setState((prevState) => {
return { ...prevState, F2Key: false }
})
return
}
}
treeRef.current?.addEventListener('keydown', F2KeyPressHandler)
treeRef.current?.addEventListener('keyup', F2KeyPressUpHandler)
return () => {
treeRef.current?.removeEventListener('keydown', F2KeyPressHandler)
treeRef.current?.removeEventListener('keyup', F2KeyPressUpHandler)
}
}
}, [treeRef.current, feTarget])
const hasReservedKeyword = (content: string): boolean => { const hasReservedKeyword = (content: string): boolean => {
if (state.reservedKeywords.findIndex((value) => content.startsWith(value)) !== -1) return true if (state.reservedKeywords.findIndex((value) => content.startsWith(value)) !== -1) return true
else return false else return false
@ -292,17 +392,18 @@ export const FileExplorer = (props: FileExplorerProps) => {
props.dispatchHandleExpandPath(expandPath) props.dispatchHandleExpandPath(expandPath)
} }
const handleFileMove = async (dest: string, src: string) => { /**
* This offers the ability to move a file to a new location
* without showing a modal dialong to the user.
* @param dest path of the destination
* @param src path of the source
* @returns {Promise<void>}
*/
const moveFileSilently = async (dest: string, src: string) => {
if (dest.length === 0 || src.length === 0) return
if (await moveFileIsAllowed(src, dest) === false) return if (await moveFileIsAllowed(src, dest) === false) return
try { try {
props.modal( props.dispatchMoveFile(src, dest)
intl.formatMessage({ id: 'filePanel.moveFile' }),
intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src, dest }),
intl.formatMessage({ id: 'filePanel.yes' }),
() => props.dispatchMoveFile(src, dest),
intl.formatMessage({ id: 'filePanel.cancel' }),
() => { }
)
} catch (error) { } catch (error) {
props.modal( props.modal(
intl.formatMessage({ id: 'filePanel.movingFileFailed' }), intl.formatMessage({ id: 'filePanel.movingFileFailed' }),
@ -313,17 +414,24 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
} }
const handleFolderMove = async (dest: string, src: string) => { const resetMultiselect = () => {
setState((prevState) => {
return { ...prevState, ctrlKey: false }
})
}
/**
* This offers the ability to move a folder to a new location
* without showing a modal dialong to the user.
* @param dest path of the destination
* @param src path of the source
* @returns {Promise<void>}
*/
const moveFolderSilently = async (dest: string, src: string) => {
if (dest.length === 0 || src.length === 0) return
if (await moveFolderIsAllowed(src, dest) === false) return if (await moveFolderIsAllowed(src, dest) === false) return
try { try {
props.modal( props.dispatchMoveFolder(src, dest)
intl.formatMessage({ id: 'filePanel.moveFile' }),
intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src, dest }),
intl.formatMessage({ id: 'filePanel.yes' }),
() => props.dispatchMoveFolder(src, dest),
intl.formatMessage({ id: 'filePanel.cancel' }),
() => { }
)
} catch (error) { } catch (error) {
props.modal( props.modal(
intl.formatMessage({ id: 'filePanel.movingFolderFailed' }), intl.formatMessage({ id: 'filePanel.movingFolderFailed' }),
@ -334,6 +442,19 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
} }
const warnMovingItems = async (src: string[], dest: string): Promise<void> => {
return new Promise((resolve, reject) => {
props.modal(
intl.formatMessage({ id: 'filePanel.moveFile' }),
intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src: src.join(', '), dest }),
intl.formatMessage({ id: 'filePanel.yes' }),
() => resolve(null),
intl.formatMessage({ id: 'filePanel.cancel' }),
() => reject()
)
})
}
const handleTreeClick = (event: SyntheticEvent) => { const handleTreeClick = (event: SyntheticEvent) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
while (target && target.getAttribute && !target.getAttribute('data-path')) { while (target && target.getAttribute && !target.getAttribute('data-path')) {
@ -401,13 +522,18 @@ export const FileExplorer = (props: FileExplorerProps) => {
fileState={fileState} fileState={fileState}
expandPath={props.expandPath} expandPath={props.expandPath}
handleContextMenu={handleContextMenu} handleContextMenu={handleContextMenu}
moveFile={handleFileMove} warnMovingItems={warnMovingItems}
moveFolder={handleFolderMove} moveFolderSilently={moveFolderSilently}
moveFileSilently={moveFileSilently}
resetMultiselect={resetMultiselect}
setFilesSelected={setFilesSelected}
handleClickFolder={handleClickFolder} handleClickFolder={handleClickFolder}
createNewFile={props.createNewFile} createNewFile={props.createNewFile}
createNewFolder={props.createNewFolder} createNewFolder={props.createNewFolder}
deletePath={deletePath} deletePath={deletePath}
editPath={props.editModeOn} editPath={props.editModeOn}
fileTarget={feTarget}
setTargetFiles={setFeTarget}
/> />
</div> </div>
</div> </div>

@ -1,19 +1,12 @@
import React, { SyntheticEvent, useEffect, useRef, useState } from 'react' import React, { SyntheticEvent, useContext, useEffect, useRef, useState } from 'react'
import { FileType } from '../types' import { DragStructure, FileType, FlatTreeDropProps } from '../types'
import { getEventTarget } from '../utils/getEventTarget' import { buildMultiSelectedItemProfiles, getEventTarget } from '../utils/getEventTarget'
import { extractParentFromKey } from '@remix-ui/helper' import { extractParentFromKey } from '@remix-ui/helper'
interface FlatTreeDropProps { import { FileSystemContext } from '../contexts'
moveFile: (dest: string, src: string) => void
moveFolder: (dest: string, src: string) => void
getFlatTreeItem: (path: string) => FileType
handleClickFolder: (path: string, type: string) => void
dragSource: FileType
children: React.ReactNode
expandPath: string[]
}
export const FlatTreeDrop = (props: FlatTreeDropProps) => { export const FlatTreeDrop = (props: FlatTreeDropProps) => {
const { getFlatTreeItem, dragSource, moveFile, moveFolder, handleClickFolder, expandPath } = props const { getFlatTreeItem, dragSource, handleClickFolder, expandPath } = props
// delay timer // delay timer
const [timer, setTimer] = useState<NodeJS.Timeout>() const [timer, setTimer] = useState<NodeJS.Timeout>()
// folder to open // folder to open
@ -21,7 +14,9 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => {
const onDragOver = async (e: SyntheticEvent) => { const onDragOver = async (e: SyntheticEvent) => {
e.preventDefault() e.preventDefault()
const target = await getEventTarget(e) const target = await getEventTarget(e)
if (!target || !target.path) { if (!target || !target.path) {
clearTimeout(timer) clearTimeout(timer)
setFolderToOpen(null) setFolderToOpen(null)
@ -50,6 +45,8 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => {
event.preventDefault() event.preventDefault()
const target = await getEventTarget(event) const target = await getEventTarget(event)
const filePaths = []
let dragDestination: any let dragDestination: any
if (!target || !target.path) { if (!target || !target.path) {
dragDestination = { dragDestination = {
@ -59,23 +56,39 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => {
} else { } else {
dragDestination = getFlatTreeItem(target.path) dragDestination = getFlatTreeItem(target.path)
} }
props.selectedItems.forEach((item) => filePaths.push(item.path))
props.setFilesSelected(filePaths)
if (dragDestination.isDirectory) { if (dragDestination.isDirectory) {
if (dragSource.isDirectory) { await props.warnMovingItems(filePaths, dragDestination.path)
moveFolder(dragDestination.path, dragSource.path) await moveItemsSilently(props.selectedItems, dragDestination.path)
} else {
moveFile(dragDestination.path, dragSource.path)
}
} else { } else {
const path = extractParentFromKey(dragDestination.path) || '/' const path = extractParentFromKey(dragDestination.path) || '/'
await props.warnMovingItems(filePaths, path)
if (dragSource.isDirectory) { await moveItemsSilently(props.selectedItems, path)
moveFolder(path, dragSource.path)
} else {
moveFile(path, dragSource.path)
}
} }
} }
/**
* Moves items silently without showing a confirmation dialog.
* @param items MultiSelected items built into a DragStructure profile
* @param dragSource source FileExplorer item being dragged.
* @returns Promise<void>
*/
const moveItemsSilently = async (items: DragStructure[], targetPath: string) => {
const promises = items.filter(item => item.path !== targetPath)
.map(async (item) => {
if (item.type === 'file') {
await props.moveFileSilently(targetPath, item.path)
} else if (item.type === 'folder') {
await props.moveFolderSilently(targetPath, item.path)
}
})
await Promise.all(promises)
props.resetMultiselect()
}
return (<div return (<div
onDrop={onDrop} onDragOver={onDragOver} onDrop={onDrop} onDragOver={onDragOver}
className="d-flex h-100" className="d-flex h-100"

@ -1,14 +1,15 @@
import React, { SyntheticEvent, useEffect, useRef, useState, RefObject, useMemo } from 'react' import React, { SyntheticEvent, useEffect, useRef, useState, RefObject, useMemo, useContext, Dispatch } from 'react'
import { Popover } from 'react-bootstrap' import { Popover } from 'react-bootstrap'
import { FileType, WorkspaceElement } from '../types' import { DragStructure, FileType, WorkspaceElement } from '../types'
import { getPathIcon } from '@remix-ui/helper'; import { getPathIcon } from '@remix-ui/helper';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso' import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
import { FlatTreeItemInput } from './flat-tree-item-input'; import { FlatTreeItemInput } from './flat-tree-item-input';
import { FlatTreeDrop } from './flat-tree-drop'; import { FlatTreeDrop } from './flat-tree-drop';
import { getEventTarget } from '../utils/getEventTarget'; import { buildMultiSelectedItemProfiles, getEventTarget } from '../utils/getEventTarget';
import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators'; import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators';
import { FileHoverIcons } from './file-explorer-hovericons'; import { FileHoverIcons } from './file-explorer-hovericons';
import { deletePath } from '../actions'; import { deletePath } from '../actions';
import { FileSystemContext } from '../contexts';
export default function useOnScreen(ref: RefObject<HTMLElement>) { export default function useOnScreen(ref: RefObject<HTMLElement>) {
@ -25,6 +26,8 @@ export default function useOnScreen(ref: RefObject<HTMLElement>) {
return isIntersecting return isIntersecting
} }
interface FlatTreeProps { interface FlatTreeProps {
fileTarget: any
setTargetFiles: React.Dispatch<any>
files: { [x: string]: Record<string, FileType> }, files: { [x: string]: Record<string, FileType> },
flatTree: FileType[], flatTree: FileType[],
expandPath: string[], expandPath: string[],
@ -35,13 +38,16 @@ interface FlatTreeProps {
handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void
handleTreeClick: (e: SyntheticEvent) => void handleTreeClick: (e: SyntheticEvent) => void
handleClickFolder: (path: string, type: string) => void handleClickFolder: (path: string, type: string) => void
moveFile: (dest: string, src: string) => void moveFolderSilently: (dest: string, src: string) => Promise<void>
moveFolder: (dest: string, src: string) => void moveFileSilently: (dest: string, src: string) => Promise<void>
resetMultiselect: () => void
setFilesSelected: Dispatch<React.SetStateAction<string[]>>
fileState: fileDecoration[] fileState: fileDecoration[]
createNewFile?: any createNewFile?: any
createNewFolder?: any createNewFolder?: any
deletePath?: (path: string | string[]) => void | Promise<void> deletePath?: (path: string | string[]) => void | Promise<void>
editPath?: (path: string, type: string, isNew?: boolean) => void editPath?: (path: string, type: string, isNew?: boolean) => void
warnMovingItems: (srcs: string[], dests: string) => Promise<void>
} }
let mouseTimer: any = { let mouseTimer: any = {
@ -50,7 +56,7 @@ let mouseTimer: any = {
} }
export const FlatTree = (props: FlatTreeProps) => { export const FlatTree = (props: FlatTreeProps) => {
const { files, flatTree, expandPath, focusEdit, editModeOff, handleTreeClick, moveFile, moveFolder, fileState, focusElement, handleClickFolder, deletePath, editPath } = props const { files, flatTree, expandPath, focusEdit, editModeOff, handleTreeClick, warnMovingItems, fileState, focusElement, handleClickFolder, deletePath, moveFileSilently, moveFolderSilently, setFilesSelected } = props
const [hover, setHover] = useState<string>('') const [hover, setHover] = useState<string>('')
const [mouseOverTarget, setMouseOverTarget] = useState<{ const [mouseOverTarget, setMouseOverTarget] = useState<{
path: string, path: string,
@ -67,18 +73,25 @@ export const FlatTree = (props: FlatTreeProps) => {
const ref = useRef(null) const ref = useRef(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const virtuoso = useRef<VirtuosoHandle>(null) const virtuoso = useRef<VirtuosoHandle>(null)
const [selectedItems, setSelectedItems] = useState<DragStructure[]>([])
const labelClass = (file: FileType) => const labelClass = (file: FileType) =>
props.focusEdit.element === file.path props.focusEdit.element === file.path
? 'bg-light' ? 'bg-light'
: props.focusElement.findIndex((item) => item.key === file.path) !== -1 : props.focusElement.findIndex((item) => item.key === file.path) !== -1
? 'bg-secondary' ? 'bg-secondary remixui_selected'
: hover == file.path : hover == file.path
? 'bg-light border-no-shift' ? 'bg-light border-no-shift'
: props.focusContext.element === file.path && props.focusEdit.element !== file.path : props.focusContext.element === file.path && props.focusEdit.element !== file.path
? 'bg-light border-no-shift' ? 'bg-light border-no-shift'
: '' : ''
useEffect(() => {
if (props.focusElement && props.focusElement.length > 0) {
props.setTargetFiles(props.focusElement)
}
}, [props.focusElement, props.focusElement.length])
const getIndentLevelDiv = (path: string) => { const getIndentLevelDiv = (path: string) => {
// remove double slash // remove double slash
path = path.replace(/\/\//g, '/') path = path.replace(/\/\//g, '/')
@ -103,6 +116,9 @@ export const FlatTree = (props: FlatTreeProps) => {
const target = await getEventTarget(event) const target = await getEventTarget(event)
setDragSource(flatTree.find((item) => item.path === target.path)) setDragSource(flatTree.find((item) => item.path === target.path))
setIsDragging(true) setIsDragging(true)
const items = buildMultiSelectedItemProfiles(target)
setSelectedItems(items)
setFilesSelected(items.map((item) => item.path))
} }
useEffect(() => { useEffect(() => {
@ -116,6 +132,12 @@ export const FlatTree = (props: FlatTreeProps) => {
const onDragEnd = (event: SyntheticEvent) => { const onDragEnd = (event: SyntheticEvent) => {
setIsDragging(false) setIsDragging(false)
document.querySelectorAll('li.remixui_selected').forEach(item => {
item.classList.remove('remixui_selected')
item.classList.remove('bg-secondary')
})
props.setFilesSelected([])
setSelectedItems([])
} }
const getFlatTreeItem = (path: string) => { const getFlatTreeItem = (path: string) => {
@ -247,17 +269,23 @@ export const FlatTree = (props: FlatTreeProps) => {
<FlatTreeDrop <FlatTreeDrop
dragSource={dragSource} dragSource={dragSource}
getFlatTreeItem={getFlatTreeItem} getFlatTreeItem={getFlatTreeItem}
moveFile={moveFile} warnMovingItems={warnMovingItems}
moveFolder={moveFolder} moveFolderSilently={moveFolderSilently}
moveFileSilently={moveFileSilently}
resetMultiselect={props.resetMultiselect}
setFilesSelected={setFilesSelected}
handleClickFolder={handleClickFolder} handleClickFolder={handleClickFolder}
expandPath={expandPath} expandPath={expandPath}
selectedItems={selectedItems}
setSelectedItems={setSelectedItems}
> >
<div data-id="treeViewUltreeViewMenu" <div data-id="treeViewUltreeViewMenu"
className='d-flex h-100 w-100 pb-2' className='d-flex h-100 w-100 pb-2'
onClick={handleTreeClick} onClick={handleTreeClick}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
onContextMenu={handleContextMenu}> onContextMenu={handleContextMenu}
>
{ showMouseOverTarget && mouseOverTarget && !isDragging && { showMouseOverTarget && mouseOverTarget && !isDragging &&
<Popover id='popover-basic' <Popover id='popover-basic'
placement='top' placement='top'

@ -40,6 +40,8 @@ export const FileSystemContext = createContext<{
dispatchCloneRepository: (url: string) => Promise<void>, dispatchCloneRepository: (url: string) => Promise<void>,
dispatchMoveFile: (src: string, dest: string) => Promise<void>, dispatchMoveFile: (src: string, dest: string) => Promise<void>,
dispatchMoveFolder: (src: string, dest: string) => Promise<void>, dispatchMoveFolder: (src: string, dest: string) => Promise<void>,
dispatchMoveFiles: (src: string[], dest: string) => Promise<void>,
dispatchMoveFolders: (src: string[], dest: string) => Promise<void>,
dispatchShowAllBranches: () => Promise<void>, dispatchShowAllBranches: () => Promise<void>,
dispatchSwitchToBranch: (branch: branch) => Promise<void>, dispatchSwitchToBranch: (branch: branch) => Promise<void>,
dispatchCreateNewBranch: (name: string) => Promise<void>, dispatchCreateNewBranch: (name: string) => Promise<void>,

@ -69,3 +69,7 @@ ul {
.remixui_icons:hover { .remixui_icons:hover {
color: var(--text); color: var(--text);
} }
.remixui_selected {
}

@ -202,10 +202,22 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await moveFile(src, dest) await moveFile(src, dest)
} }
const dispatchMoveFiles = async (src: string[], dest: string) => {
for (const path of src) {
await moveFile(path, dest)
}
}
const dispatchMoveFolder = async (src: string, dest: string) => { const dispatchMoveFolder = async (src: string, dest: string) => {
await moveFolder(src, dest) await moveFolder(src, dest)
} }
const dispatchMoveFolders = async (src: string[], dest: string) => {
for (const path of src) {
await moveFolder(path, dest)
}
}
const dispatchShowAllBranches = async () => { const dispatchShowAllBranches = async () => {
await showAllBranches() await showAllBranches()
} }
@ -368,7 +380,9 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchHandleRestoreBackup, dispatchHandleRestoreBackup,
dispatchCloneRepository, dispatchCloneRepository,
dispatchMoveFile, dispatchMoveFile,
dispatchMoveFiles,
dispatchMoveFolder, dispatchMoveFolder,
dispatchMoveFolders,
dispatchShowAllBranches, dispatchShowAllBranches,
dispatchSwitchToBranch, dispatchSwitchToBranch,
dispatchCreateNewBranch, dispatchCreateNewBranch,

@ -1253,7 +1253,9 @@ export function Workspace() {
dispatchAddInputField={global.dispatchAddInputField} dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath} dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile} dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFiles={global.dispatchMoveFiles}
dispatchMoveFolder={global.dispatchMoveFolder} dispatchMoveFolder={global.dispatchMoveFolder}
dispatchMoveFolders={global.dispatchMoveFolders}
handleCopyClick={handleCopyClick} handleCopyClick={handleCopyClick}
handlePasteClick={handlePasteClick} handlePasteClick={handlePasteClick}
addMenuItems={addMenuItems} addMenuItems={addMenuItems}
@ -1319,7 +1321,9 @@ export function Workspace() {
dispatchAddInputField={global.dispatchAddInputField} dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath} dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile} dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFiles={global.dispatchMoveFiles}
dispatchMoveFolder={global.dispatchMoveFolder} dispatchMoveFolder={global.dispatchMoveFolder}
dispatchMoveFolders={global.dispatchMoveFolders}
handleCopyClick={handleCopyClick} handleCopyClick={handleCopyClick}
handlePasteClick={handlePasteClick} handlePasteClick={handlePasteClick}
addMenuItems={addMenuItems} addMenuItems={addMenuItems}

@ -1,5 +1,5 @@
/* eslint-disable @nrwl/nx/enforce-module-boundaries */ /* eslint-disable @nrwl/nx/enforce-module-boundaries */
import React from 'react' import React, { Dispatch } from 'react'
import { customAction } from '@remixproject/plugin-api' import { customAction } from '@remixproject/plugin-api'
import { fileDecoration } from '@remix-ui/file-decorators' import { fileDecoration } from '@remix-ui/file-decorators'
import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types' import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types'
@ -136,6 +136,8 @@ export interface FileExplorerProps {
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>, dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: string[]) => Promise<void>, dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchMoveFile: (src: string, dest: string) => Promise<void>, dispatchMoveFile: (src: string, dest: string) => Promise<void>,
dispatchMoveFiles: (src: string[], dest: string) => Promise<void>,
dispatchMoveFolders: (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 handlePasteClick: (dest: string, destType: string) => void
handleCopyClick: (path: string, type: WorkspaceElement) => void handleCopyClick: (path: string, type: WorkspaceElement) => void
@ -201,6 +203,8 @@ export interface FileExplorerContextMenuProps {
export interface WorkSpaceState { export interface WorkSpaceState {
ctrlKey: boolean ctrlKey: boolean
deleteKey?: boolean
F2Key?: boolean
newFileName: string newFileName: string
actions: { actions: {
id: string id: string
@ -343,3 +347,28 @@ export interface Action<T extends keyof ActionPayloadTypes> {
export type Actions = {[A in keyof ActionPayloadTypes]: Action<A>}[keyof ActionPayloadTypes] export type Actions = {[A in keyof ActionPayloadTypes]: Action<A>}[keyof ActionPayloadTypes]
export type WorkspaceElement = 'folder' | 'file' | 'workspace' export type WorkspaceElement = 'folder' | 'file' | 'workspace'
export interface FlatTreeDropProps {
resetMultiselect: () => void
moveFolderSilently: (dest: string, src: string) => Promise<void>
moveFileSilently: (dest: string, src: string) => Promise<void>
setFilesSelected: Dispatch<React.SetStateAction<string[]>>
getFlatTreeItem: (path: string) => FileType
handleClickFolder: (path: string, type: string) => void
dragSource: FileType
children: React.ReactNode
expandPath: string[]
selectedItems: DragStructure[]
setSelectedItems: Dispatch<React.SetStateAction<DragStructure[]>>
warnMovingItems: (srcs: string[], dest: string) => Promise<void>
}
export type DragStructure = {
position: {
top: number
left: number
}
path: string
type: string
content: string
}

@ -22,4 +22,35 @@ export const getEventTarget = async (e: any, useLabel: boolean = false) => {
position: endPosition position: endPosition
} }
} }
} }
/**
* When multiple files are selected in FileExplorer,
* and these files are dragged to a target folder,
* this function will build the profile of each selected item
* in FileExplorer so they can be moved when dropped
* @param target - Initial target item in FileExplorer
* @returns - {DragStructure} Array of selected items
*/
export const buildMultiSelectedItemProfiles = (target: {
path: string
type: string
content: string
position: {
top: number
left: number
}
}) => {
const selectItems = []
selectItems.push(target)
document.querySelectorAll('li.remixui_selected').forEach(item => {
const dragTarget = {
position: { top: target?.position.top || 0, left: target?.position.left || 0 },
path: item.getAttribute('data-path') || item.getAttribute('data-label-path') || '',
type: item.getAttribute('data-type') || item.getAttribute('data-label-type') || '',
content: item.textContent || ''
}
if (dragTarget.path !== target.path) selectItems.push(dragTarget)
})
return selectItems
}

Loading…
Cancel
Save