Merge pull request #4767 from ethereum/multiselect-dragdrop

Add Multiselect dragdrop Feature
pull/4994/head
Joseph Izang 4 months ago committed by GitHub
commit 60a00e550b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      apps/remix-ide-e2e/src/commands/selectFiles.ts
  2. 77
      apps/remix-ide-e2e/src/tests/file_explorer_multiselect.test.ts
  3. 12
      apps/remix-ide/src/app/files/fileManager.ts
  4. 18
      libs/remix-ui/workspace/src/lib/actions/index.ts
  5. 67
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  6. 59
      libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx
  7. 40
      libs/remix-ui/workspace/src/lib/components/flat-tree.tsx
  8. 2
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  9. 4
      libs/remix-ui/workspace/src/lib/css/file-explorer.css
  10. 14
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  11. 4
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  12. 29
      libs/remix-ui/workspace/src/lib/types/index.ts
  13. 31
      libs/remix-ui/workspace/src/lib/utils/getEventTarget.ts

@ -11,12 +11,11 @@ class SelectFiles extends EventEmitter {
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

@ -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())
})
})
}
}
}

@ -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)

@ -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
}

@ -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>(workspaceState)
// const [isPending, startTransition] = useTransition();
const treeRef = useRef<HTMLDivElement>(null)
const [filesSelected, setFilesSelected] = useState<string[]>([])
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<void>}
*/
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<void>}
*/
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<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) => {
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}

@ -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<NodeJS.Timeout>()
// 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,21 +56,37 @@ 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<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

@ -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 { FileType, WorkspaceElement } from '../types'
import { DragStructure, FileType, WorkspaceElement } from '../types'
import { getPathIcon } from '@remix-ui/helper';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
import { FlatTreeItemInput } from './flat-tree-item-input';
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 { FileHoverIcons } from './file-explorer-hovericons';
import { deletePath } from '../actions';
import { FileSystemContext } from '../contexts';
export default function useOnScreen(ref: RefObject<HTMLElement>) {
@ -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<void>
moveFileSilently: (dest: string, src: string) => Promise<void>
resetMultiselect: () => void
setFilesSelected: Dispatch<React.SetStateAction<string[]>>
fileState: fileDecoration[]
createNewFile?: any
createNewFolder?: any
deletePath?: (path: string | string[]) => void | Promise<void>
editPath?: (path: string, type: string, isNew?: boolean) => void
warnMovingItems: (srcs: string[], dests: string) => Promise<void>
}
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<string>('')
const [mouseOverTarget, setMouseOverTarget] = useState<{
path: string,
@ -67,12 +71,13 @@ export const FlatTree = (props: FlatTreeProps) => {
const ref = useRef(null)
const containerRef = useRef<HTMLDivElement>(null)
const virtuoso = useRef<VirtuosoHandle>(null)
const [selectedItems, setSelectedItems] = useState<DragStructure[]>([])
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) => {
<FlatTreeDrop
dragSource={dragSource}
getFlatTreeItem={getFlatTreeItem}
moveFile={moveFile}
moveFolder={moveFolder}
warnMovingItems={warnMovingItems}
moveFolderSilently={moveFolderSilently}
moveFileSilently={moveFileSilently}
resetMultiselect={props.resetMultiselect}
setFilesSelected={setFilesSelected}
handleClickFolder={handleClickFolder}
expandPath={expandPath}
selectedItems={selectedItems}
setSelectedItems={setSelectedItems}
>
<div data-id="treeViewUltreeViewMenu"
className='d-flex h-100 w-100 pb-2'
onClick={handleTreeClick}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
onContextMenu={handleContextMenu}>
onContextMenu={handleContextMenu}
>
{ showMouseOverTarget && mouseOverTarget && !isDragging &&
<Popover id='popover-basic'
placement='top'

@ -40,6 +40,8 @@ export const FileSystemContext = createContext<{
dispatchCloneRepository: (url: string) => Promise<void>,
dispatchMoveFile: (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>,
dispatchSwitchToBranch: (branch: branch) => Promise<void>,
dispatchCreateNewBranch: (name: string) => Promise<void>,

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

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

@ -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}

@ -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<void>,
dispatchHandleExpandPath: (paths: 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>,
handlePasteClick: (dest: string, destType: string) => void
handleCopyClick: (path: string, type: WorkspaceElement) => void
@ -343,3 +345,28 @@ export interface Action<T extends keyof ActionPayloadTypes> {
export type Actions = {[A in keyof ActionPayloadTypes]: Action<A>}[keyof ActionPayloadTypes]
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
}

@ -23,3 +23,34 @@ export const getEventTarget = async (e: any, useLabel: boolean = false) => {
}
}
}
/**
* 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