diff --git a/apps/remix-ide-e2e/src/commands/selectFiles.ts b/apps/remix-ide-e2e/src/commands/selectFiles.ts index d24ec1e42b..544f36ba3d 100644 --- a/apps/remix-ide-e2e/src/commands/selectFiles.ts +++ b/apps/remix-ide-e2e/src/commands/selectFiles.ts @@ -8,15 +8,14 @@ class SelectFiles extends EventEmitter { browser.perform(function () { const actions = this.actions({ async: true }) 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) } - return actions.contextClick(selectedElements[0].value) + return actions//.contextClick(selectedElements[0].value) }) this.emit('complete') return this } } - module.exports = SelectFiles diff --git a/apps/remix-ide-e2e/src/tests/file_explorer_multiselect.test.ts b/apps/remix-ide-e2e/src/tests/file_explorer_multiselect.test.ts index 1d31d6a9b5..214217af38 100644 --- a/apps/remix-ide-e2e/src/tests/file_explorer_multiselect.test.ts +++ b/apps/remix-ide-e2e/src/tests/file_explorer_multiselect.test.ts @@ -2,6 +2,7 @@ import { NightwatchBrowser } from 'nightwatch' import init from '../helpers/init' module.exports = { + "@disabled": true, before: function (browser: NightwatchBrowser, done: VoidFunction) { init(browser, done) }, @@ -10,11 +11,11 @@ module.exports = { const selectedElements = [] browser .openFile('contracts') - .click({ selector: '//*[@data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]', locateStrategy: 'xpath' }) - .findElement({ selector: '//*[@data-id="treeViewLitreeViewItemcontracts/2_Owner.sol"]', locateStrategy: 'xpath' }, (el) => { + .click({ selector: '//*[@data-id="treeViewDivtreeViewItemcontracts/1_Storage.sol"]', locateStrategy: 'xpath' }) + .findElement({ selector: '//*[@data-id="treeViewDivtreeViewItemcontracts/2_Owner.sol"]', locateStrategy: 'xpath' }, (el) => { selectedElements.push(el) }) - browser.findElement({ selector: '//*[@data-id="treeViewLitreeViewItemtests"]', locateStrategy: 'xpath' }, + browser.findElement({ selector: '//*[@data-id="treeViewDivtreeViewItemtests"]', locateStrategy: 'xpath' }, (el: any) => { 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/2_Owner.sol"]') .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()) + }) + }) + } } } diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index e232c2a132..229c15ce33 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -962,6 +962,12 @@ class FileManager extends Plugin { 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) { try { 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) { try { src = this.normalize(src) diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts index b4480e2019..772bdff1a6 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -656,3 +656,21 @@ export const moveFolderIsAllowed = async (src: string, dest: string) => { 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 +} + diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx index e3b301901c..10dea83d97 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -9,7 +9,7 @@ import '../css/file-explorer.css' import { checkSpecialChars, extractNameFromKey, extractParentFromKey, getPathIcon, joinPath } from '@remix-ui/helper' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ROOT_PATH } from '../utils/constants' -import { moveFileIsAllowed, moveFolderIsAllowed } from '../actions' +import { moveFileIsAllowed, moveFilesIsAllowed, moveFolderIsAllowed, moveFoldersIsAllowed } from '../actions' import { FlatTree } from './flat-tree' export const FileExplorer = (props: FileExplorerProps) => { @@ -35,6 +35,7 @@ export const FileExplorer = (props: FileExplorerProps) => { const [state, setState] = useState(workspaceState) // const [isPending, startTransition] = useTransition(); const treeRef = useRef(null) + const [filesSelected, setFilesSelected] = useState([]) useEffect(() => { if (contextMenuItems) { @@ -292,17 +293,18 @@ export const FileExplorer = (props: FileExplorerProps) => { 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} + */ + const moveFileSilently = async (dest: string, src: string) => { + if (dest.length === 0 || src.length === 0) return if (await moveFileIsAllowed(src, dest) === false) return try { - props.modal( - 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' }), - () => { } - ) + props.dispatchMoveFile(src, dest) } catch (error) { props.modal( intl.formatMessage({ id: 'filePanel.movingFileFailed' }), @@ -313,17 +315,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} + */ + const moveFolderSilently = async (dest: string, src: string) => { + if (dest.length === 0 || src.length === 0) return if (await moveFolderIsAllowed(src, dest) === false) return try { - props.modal( - 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' }), - () => { } - ) + props.dispatchMoveFolder(src, dest) } catch (error) { props.modal( intl.formatMessage({ id: 'filePanel.movingFolderFailed' }), @@ -334,6 +343,19 @@ export const FileExplorer = (props: FileExplorerProps) => { } } + const warnMovingItems = async (src: string[], dest: string): Promise => { + 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) => { let target = event.target as HTMLElement while (target && target.getAttribute && !target.getAttribute('data-path')) { @@ -401,8 +423,11 @@ export const FileExplorer = (props: FileExplorerProps) => { fileState={fileState} expandPath={props.expandPath} handleContextMenu={handleContextMenu} - moveFile={handleFileMove} - moveFolder={handleFolderMove} + warnMovingItems={warnMovingItems} + moveFolderSilently={moveFolderSilently} + moveFileSilently={moveFileSilently} + resetMultiselect={resetMultiselect} + setFilesSelected={setFilesSelected} handleClickFolder={handleClickFolder} createNewFile={props.createNewFile} createNewFolder={props.createNewFolder} diff --git a/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx b/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx index 92de1ff8f1..d049a2bb6c 100644 --- a/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx +++ b/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx @@ -1,19 +1,12 @@ -import React, { SyntheticEvent, useEffect, useRef, useState } from 'react' -import { FileType } from '../types' -import { getEventTarget } from '../utils/getEventTarget' +import React, { SyntheticEvent, useContext, useEffect, useRef, useState } from 'react' +import { DragStructure, FileType, FlatTreeDropProps } from '../types' +import { buildMultiSelectedItemProfiles, getEventTarget } from '../utils/getEventTarget' import { extractParentFromKey } from '@remix-ui/helper' -interface FlatTreeDropProps { - 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[] -} +import { FileSystemContext } from '../contexts' + export const FlatTreeDrop = (props: FlatTreeDropProps) => { - const { getFlatTreeItem, dragSource, moveFile, moveFolder, handleClickFolder, expandPath } = props + const { getFlatTreeItem, dragSource, handleClickFolder, expandPath } = props // delay timer const [timer, setTimer] = useState() // folder to open @@ -21,7 +14,9 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => { const onDragOver = async (e: SyntheticEvent) => { e.preventDefault() + const target = await getEventTarget(e) + if (!target || !target.path) { clearTimeout(timer) setFolderToOpen(null) @@ -50,6 +45,8 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => { event.preventDefault() const target = await getEventTarget(event) + const filePaths = [] + let dragDestination: any if (!target || !target.path) { dragDestination = { @@ -59,23 +56,39 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => { } else { dragDestination = getFlatTreeItem(target.path) } + + props.selectedItems.forEach((item) => filePaths.push(item.path)) + props.setFilesSelected(filePaths) + if (dragDestination.isDirectory) { - if (dragSource.isDirectory) { - moveFolder(dragDestination.path, dragSource.path) - } else { - moveFile(dragDestination.path, dragSource.path) - } + await props.warnMovingItems(filePaths, dragDestination.path) + await moveItemsSilently(props.selectedItems, dragDestination.path) } else { const path = extractParentFromKey(dragDestination.path) || '/' - - if (dragSource.isDirectory) { - moveFolder(path, dragSource.path) - } else { - moveFile(path, dragSource.path) - } + await props.warnMovingItems(filePaths, path) + await moveItemsSilently(props.selectedItems, 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 + */ + 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 (
) { @@ -35,13 +36,16 @@ interface FlatTreeProps { handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void handleTreeClick: (e: SyntheticEvent) => void handleClickFolder: (path: string, type: string) => void - moveFile: (dest: string, src: string) => void - moveFolder: (dest: string, src: string) => void + moveFolderSilently: (dest: string, src: string) => Promise + moveFileSilently: (dest: string, src: string) => Promise + resetMultiselect: () => void + setFilesSelected: Dispatch> fileState: fileDecoration[] createNewFile?: any createNewFolder?: any deletePath?: (path: string | string[]) => void | Promise editPath?: (path: string, type: string, isNew?: boolean) => void + warnMovingItems: (srcs: string[], dests: string) => Promise } let mouseTimer: any = { @@ -50,7 +54,7 @@ let mouseTimer: any = { } 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('') const [mouseOverTarget, setMouseOverTarget] = useState<{ path: string, @@ -67,12 +71,13 @@ export const FlatTree = (props: FlatTreeProps) => { const ref = useRef(null) const containerRef = useRef(null) const virtuoso = useRef(null) + const [selectedItems, setSelectedItems] = useState([]) const labelClass = (file: FileType) => props.focusEdit.element === file.path ? 'bg-light' : props.focusElement.findIndex((item) => item.key === file.path) !== -1 - ? 'bg-secondary' + ? 'bg-secondary remixui_selected' : hover == file.path ? 'bg-light border-no-shift' : props.focusContext.element === file.path && props.focusEdit.element !== file.path @@ -103,6 +108,9 @@ export const FlatTree = (props: FlatTreeProps) => { const target = await getEventTarget(event) setDragSource(flatTree.find((item) => item.path === target.path)) setIsDragging(true) + const items = buildMultiSelectedItemProfiles(target) + setSelectedItems(items) + setFilesSelected(items.map((item) => item.path)) } useEffect(() => { @@ -116,6 +124,12 @@ export const FlatTree = (props: FlatTreeProps) => { const onDragEnd = (event: SyntheticEvent) => { 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) => { @@ -247,17 +261,23 @@ export const FlatTree = (props: FlatTreeProps) => {
+ onContextMenu={handleContextMenu} + > { showMouseOverTarget && mouseOverTarget && !isDragging && Promise, dispatchMoveFile: (src: string, dest: string) => Promise, dispatchMoveFolder: (src: string, dest: string) => Promise, + dispatchMoveFiles: (src: string[], dest: string) => Promise, + dispatchMoveFolders: (src: string[], dest: string) => Promise, dispatchShowAllBranches: () => Promise, dispatchSwitchToBranch: (branch: branch) => Promise, dispatchCreateNewBranch: (name: string) => Promise, diff --git a/libs/remix-ui/workspace/src/lib/css/file-explorer.css b/libs/remix-ui/workspace/src/lib/css/file-explorer.css index 16f0ffca51..4ccf2096c7 100644 --- a/libs/remix-ui/workspace/src/lib/css/file-explorer.css +++ b/libs/remix-ui/workspace/src/lib/css/file-explorer.css @@ -69,3 +69,7 @@ ul { .remixui_icons:hover { color: var(--text); } + +.remixui_selected { + +} diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index af2caab30f..7494e39e3d 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -202,10 +202,22 @@ export const FileSystemProvider = (props: WorkspaceProps) => { 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) => { await moveFolder(src, dest) } + const dispatchMoveFolders = async (src: string[], dest: string) => { + for (const path of src) { + await moveFolder(path, dest) + } + } + const dispatchShowAllBranches = async () => { await showAllBranches() } @@ -368,7 +380,9 @@ export const FileSystemProvider = (props: WorkspaceProps) => { dispatchHandleRestoreBackup, dispatchCloneRepository, dispatchMoveFile, + dispatchMoveFiles, dispatchMoveFolder, + dispatchMoveFolders, dispatchShowAllBranches, dispatchSwitchToBranch, dispatchCreateNewBranch, diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 03d807808a..5b924b9697 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -1253,7 +1253,9 @@ export function Workspace() { dispatchAddInputField={global.dispatchAddInputField} dispatchHandleExpandPath={global.dispatchHandleExpandPath} dispatchMoveFile={global.dispatchMoveFile} + dispatchMoveFiles={global.dispatchMoveFiles} dispatchMoveFolder={global.dispatchMoveFolder} + dispatchMoveFolders={global.dispatchMoveFolders} handleCopyClick={handleCopyClick} handlePasteClick={handlePasteClick} addMenuItems={addMenuItems} @@ -1319,7 +1321,9 @@ export function Workspace() { dispatchAddInputField={global.dispatchAddInputField} dispatchHandleExpandPath={global.dispatchHandleExpandPath} dispatchMoveFile={global.dispatchMoveFile} + dispatchMoveFiles={global.dispatchMoveFiles} dispatchMoveFolder={global.dispatchMoveFolder} + dispatchMoveFolders={global.dispatchMoveFolders} handleCopyClick={handleCopyClick} handlePasteClick={handlePasteClick} addMenuItems={addMenuItems} diff --git a/libs/remix-ui/workspace/src/lib/types/index.ts b/libs/remix-ui/workspace/src/lib/types/index.ts index aff1b70bd8..c00dc5e46d 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -1,5 +1,5 @@ /* eslint-disable @nrwl/nx/enforce-module-boundaries */ -import React from 'react' +import React, { Dispatch } from 'react' import { customAction } from '@remixproject/plugin-api' import { fileDecoration } from '@remix-ui/file-decorators' import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types' @@ -136,6 +136,8 @@ export interface FileExplorerProps { dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise, dispatchHandleExpandPath: (paths: string[]) => Promise, dispatchMoveFile: (src: string, dest: string) => Promise, + dispatchMoveFiles: (src: string[], dest: string) => Promise, + dispatchMoveFolders: (src: string[], dest: string) => Promise, dispatchMoveFolder: (src: string, dest: string) => Promise, handlePasteClick: (dest: string, destType: string) => void handleCopyClick: (path: string, type: WorkspaceElement) => void @@ -343,3 +345,28 @@ export interface Action { export type Actions = {[A in keyof ActionPayloadTypes]: Action}[keyof ActionPayloadTypes] export type WorkspaceElement = 'folder' | 'file' | 'workspace' + +export interface FlatTreeDropProps { + resetMultiselect: () => void + moveFolderSilently: (dest: string, src: string) => Promise + moveFileSilently: (dest: string, src: string) => Promise + setFilesSelected: Dispatch> + getFlatTreeItem: (path: string) => FileType + handleClickFolder: (path: string, type: string) => void + dragSource: FileType + children: React.ReactNode + expandPath: string[] + selectedItems: DragStructure[] + setSelectedItems: Dispatch> + warnMovingItems: (srcs: string[], dest: string) => Promise +} + +export type DragStructure = { + position: { + top: number + left: number + } + path: string + type: string + content: string +} diff --git a/libs/remix-ui/workspace/src/lib/utils/getEventTarget.ts b/libs/remix-ui/workspace/src/lib/utils/getEventTarget.ts index e61cc7a226..ea663acf11 100644 --- a/libs/remix-ui/workspace/src/lib/utils/getEventTarget.ts +++ b/libs/remix-ui/workspace/src/lib/utils/getEventTarget.ts @@ -22,4 +22,35 @@ export const getEventTarget = async (e: any, useLabel: boolean = false) => { position: endPosition } } -} \ No newline at end of file +} + +/** + * 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 +}