Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Multiselect dragdrop Feature #4767

Merged
merged 51 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
377b861
add types
joeizang Apr 25, 2024
087f998
pass props
joeizang Apr 25, 2024
73d39d1
add functions for silent moving of files
joeizang Apr 25, 2024
946ca6b
consume function to move files silently
joeizang Apr 25, 2024
9bf1d24
remove console log
joeizang Apr 25, 2024
a0533a1
fix parameter order
joeizang Apr 25, 2024
a26da45
add jsdoc comments
joeizang Apr 25, 2024
23fb65d
add functions to handle array of source paths
joeizang Apr 25, 2024
a22733d
add dispatch functions to handle multiple files
joeizang Apr 25, 2024
4c18117
refactor to handle string array
joeizang Apr 25, 2024
4d0c363
add state to track file names being moved.
joeizang Apr 25, 2024
bf631af
fix exception when files have been dropped on destination
joeizang Apr 26, 2024
b077605
changes after review
joeizang Apr 29, 2024
9430b91
change css class selector
joeizang Apr 29, 2024
33758c3
move function outside of drop component
joeizang Apr 29, 2024
8596021
clean up types removing string array parameter
joeizang Apr 29, 2024
0bc7fad
add placeholder css class for selected files
joeizang Apr 29, 2024
85bb0e8
hoist function to fileTree component
joeizang Apr 29, 2024
a19a516
rename parameter for moveSilently. cleanup comment
joeizang Apr 29, 2024
5979406
update JSDoc
joeizang Apr 29, 2024
37b0601
add console log
joeizang Apr 29, 2024
a191bb3
clean onDrop function in flat-tree
joeizang Apr 29, 2024
848c080
refactor
joeizang Apr 29, 2024
b1988c6
using path
joeizang Apr 29, 2024
ec29e3e
add warn message
yann300 Apr 29, 2024
eea789e
refactor and fix
yann300 Apr 29, 2024
50cfadf
add e2e tests
joeizang Apr 30, 2024
4ba951f
fix tests
yann300 Apr 30, 2024
2861bca
fix dragging
yann300 Apr 30, 2024
e14f863
fix test
yann300 Apr 30, 2024
ada126d
fix test
yann300 May 30, 2024
dde075e
add resetMultiselect
yann300 May 30, 2024
7351611
changes to e2e
joeizang Jun 25, 2024
6de2bd0
fix e2e
joeizang Jun 26, 2024
8cc465d
fixed more e2e
joeizang Jun 26, 2024
e9cc84e
address firefox failure
joeizang Jun 27, 2024
e91ec74
end session correctly for firefox
joeizang Jun 27, 2024
5d6cf32
fix e2e
joeizang Jun 27, 2024
5066249
Merge branch 'master' into multiselect-dragdrop
joeizang Jun 27, 2024
511de78
Merge branch 'master' into multiselect-dragdrop
joeizang Jun 28, 2024
269ecf8
Merge branch 'master' into multiselect-dragdrop
joeizang Jul 2, 2024
7cc748b
Merge branch 'master' into multiselect-dragdrop
joeizang Jul 8, 2024
b08b337
disabled test for firefox
joeizang Jul 8, 2024
a5d487c
disable flaky test
joeizang Jul 8, 2024
40ad6f1
deal with flaky test
joeizang Jul 8, 2024
dc45408
Merge branch 'master' into multiselect-dragdrop
joeizang Jul 9, 2024
117a976
Merge branch 'master' into multiselect-dragdrop
joeizang Jul 11, 2024
a774b28
disable test in firefox
joeizang Jul 11, 2024
e7fd235
disable duplicate test
joeizang Jul 11, 2024
d0b4848
skip firefox for test
joeizang Jul 11, 2024
5f2fb89
Merge branch 'master' into multiselect-dragdrop
joeizang Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions apps/remix-ide-e2e/src/commands/selectFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
77 changes: 73 additions & 4 deletions apps/remix-ide-e2e/src/tests/file_explorer_multiselect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand All @@ -10,18 +11,86 @@ 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)
})
browser.selectFiles(selectedElements)
.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())
})
})
}
}
}
12 changes: 12 additions & 0 deletions apps/remix-ide/src/app/files/fileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions libs/remix-ui/workspace/src/lib/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

67 changes: 46 additions & 21 deletions libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) {
Expand Down Expand Up @@ -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' }),
Expand All @@ -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' }),
Expand All @@ -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')) {
Expand Down Expand Up @@ -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}
Expand Down
61 changes: 37 additions & 24 deletions libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
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
const [folderToOpen, setFolderToOpen] = useState<string>()

const onDragOver = async (e: SyntheticEvent) => {
e.preventDefault()

const target = await getEventTarget(e)

if (!target || !target.path) {
clearTimeout(timer)
setFolderToOpen(null)
Expand Down Expand Up @@ -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 = {
Expand All @@ -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<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
onDrop={onDrop} onDragOver={onDragOver}
className="d-flex h-100"
Expand Down
Loading
Loading