Skip to content

Commit

Permalink
feat(renterd): bulk move files via multiselect drag interaction
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Oct 30, 2024
1 parent be794d5 commit 5f1cb4b
Show file tree
Hide file tree
Showing 10 changed files with 347 additions and 127 deletions.
5 changes: 5 additions & 0 deletions .changeset/few-sheep-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

Files and directories can now be selected and moved in bulk to a destination folder. This works even when selecting files (and entire directories) from across multiple different origin directories.
75 changes: 75 additions & 0 deletions apps/renterd-e2e/src/specs/filesDrag.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { test } from '@playwright/test'
import { navigateToBuckets } from '../fixtures/navigate'
import { createBucket, openBucket } from '../fixtures/buckets'
import {
getFileRowById,
openDirectory,
createFilesMap,
expectFilesMap,
} from '../fixtures/files'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import { hoverMouseOver, moveMouseOver } from '@siafoundation/e2e'

test.beforeEach(async ({ page }) => {
await beforeTest(page, {
hostdCount: 3,
})
})

test.afterEach(async () => {
await afterTest()
})

test('move two files by selecting and dragging from one directory out to another', async ({
page,
}) => {
test.setTimeout(120_000)
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
await createFilesMap(page, bucketName, {
'file1.txt': null,
dir1: {
'file2.txt': null,
},
dir2: {
'file3.txt': null,
'file4.txt': null,
'file5.txt': null,
},
})
await navigateToBuckets({ page })
await openBucket(page, bucketName)

await openDirectory(page, 'bucket1/dir2/')

// Select file3 and file4.
const file3 = await getFileRowById(page, 'bucket1/dir2/file3.txt', true)
await file3.click()
const file4 = await getFileRowById(page, 'bucket1/dir2/file4.txt', true)
await file4.click()

await moveMouseOver(page, file3)
await page.mouse.down()

const parentDir = await getFileRowById(page, '..', true)
await hoverMouseOver(page, parentDir)

const file1 = await getFileRowById(page, 'bucket1/file1.txt', true)
await moveMouseOver(page, file1)
await page.mouse.up()

await expectFilesMap(page, bucketName, {
'file1.txt': 'visible',
'file3.txt': 'visible',
'file4.txt': 'visible',
dir1: {
'file2.txt': 'visible',
},
dir2: {
'file5.txt': 'visible',
'file3.txt': 'hidden',
'file4.txt': 'hidden',
},
})
})
4 changes: 2 additions & 2 deletions apps/renterd/components/FilesDirectory/FilesExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function FilesExplorer() {
onDragStart,
onDragCancel,
onDragMove,
draggingObject,
draggingObjects,
} = useFilesDirectory()
const canUpload = useCanUpload()
return (
Expand Down Expand Up @@ -53,7 +53,7 @@ export function FilesExplorer() {
onDragEnd={onDragEnd}
onDragCancel={onDragCancel}
onDragMove={onDragMove}
draggingDatum={draggingObject}
draggingDatums={draggingObjects}
/>
</Dropzone>
</div>
Expand Down
37 changes: 19 additions & 18 deletions apps/renterd/contexts/filesDirectory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,6 @@ function useFilesDirectoryMain() {

const { limit, marker, isMore, response, refresh, dataset } = useDataset()

const {
onDragEnd,
onDragOver,
onDragCancel,
onDragMove,
onDragStart,
draggingObject,
} = useMove({
dataset,
activeDirectory,
setActiveDirectory,
refresh,
})

// Add parent directory to the dataset.
const _datasetPage = useMemo(() => {
if (!dataset) {
Expand Down Expand Up @@ -79,6 +65,21 @@ function useFilesDirectoryMain() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeBucket])

const {
onDragEnd,
onDragOver,
onDragCancel,
onDragMove,
onDragStart,
draggingObjects,
} = useMove({
dataset,
activeDirectory,
setActiveDirectory,
refresh,
multiSelect,
})

const datasetPageWithOnClick = useMemo(() => {
if (!_datasetPage) {
return undefined
Expand Down Expand Up @@ -106,8 +107,8 @@ function useFilesDirectoryMain() {
}
return datasetPageWithOnClick.map((d) => {
if (
draggingObject &&
draggingObject.id !== d.id &&
draggingObjects &&
draggingObjects.find((dobj) => dobj.id !== d.id) &&
d.type === 'directory'
) {
return {
Expand All @@ -120,7 +121,7 @@ function useFilesDirectoryMain() {
isDraggable: d.type !== 'bucket' && !d.isUploading,
}
})
}, [datasetPageWithOnClick, draggingObject])
}, [datasetPageWithOnClick, draggingObjects])

const dataState = useDatasetEmptyState(
dataset,
Expand Down Expand Up @@ -162,7 +163,7 @@ function useFilesDirectoryMain() {
onDragMove,
onDragCancel,
onDragOver,
draggingObject,
draggingObjects,
}
}

Expand Down
77 changes: 47 additions & 30 deletions apps/renterd/contexts/filesDirectory/move.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
} from '@dnd-kit/core'
import { FullPathSegments, getDirectorySegmentsFromPath } from '../../lib/paths'
import { useObjectsRename } from '@siafoundation/renterd-react'
import { triggerErrorToast } from '@siafoundation/design-system'
import { getMoveFileRenameParams } from '../../lib/rename'
import { MultiSelect, triggerErrorToast } from '@siafoundation/design-system'
import { getMoveFileOperations } from '../../lib/rename'

type Props = {
multiSelect: MultiSelect<ObjectData>
activeDirectory: FullPathSegments
setActiveDirectory: (
func: (directory: FullPathSegments) => FullPathSegments
Expand All @@ -24,41 +25,47 @@ type Props = {
const navigationDelay = 500

export function useMove({
multiSelect,
dataset,
activeDirectory,
setActiveDirectory,
refresh,
}: Props) {
const [draggingObject, setDraggingObject] = useState<ObjectData | undefined>(
undefined
)
const [draggingObjects, setDraggingObjects] = useState<
ObjectData[] | undefined
>(undefined)
const [, setNavTimeout] = useState<NodeJS.Timeout>()
const rename = useObjectsRename()

const moveFiles = useCallback(
async (e: DragEndEvent) => {
const { bucket, from, to, mode } = getMoveFileRenameParams(
e,
activeDirectory
)
if (from === to) {
if (!draggingObjects) {
return
}
const response = await rename.post({
payload: {
force: false,
bucket,
from,
to,
mode,
},
})
refresh()
if (response.error) {
triggerErrorToast({ title: 'Error moving files', body: response.error })
const paths = draggingObjects.map((o) => o.path)
const moveOperations = getMoveFileOperations(paths, e, activeDirectory)

for (const operation of moveOperations) {
const { bucket, from, to, mode } = operation
const response = await rename.post({
payload: {
force: false,
bucket,
from,
to,
mode,
},
})
if (response.error) {
triggerErrorToast({
title: 'Error moving files',
body: response.error,
})
}
}
refresh()
},
[refresh, rename, activeDirectory]
[refresh, rename, activeDirectory, draggingObjects]
)

const delayedNavigation = useCallback(
Expand Down Expand Up @@ -103,9 +110,19 @@ export function useMove({

const onDragStart = useCallback(
(e: DragStartEvent) => {
setDraggingObject(dataset?.find((d) => d.id === e.active.id))
// If an object included in active multiselection is dragged,
// drag the selection.
const id = String(e.active.id)
if (multiSelect.selectedIds.includes(id)) {
setDraggingObjects(
Object.entries(multiSelect.selectionMap).map(([, obj]) => obj)
)
} else {
const ob = dataset?.find((d) => d.id === e.active.id)
setDraggingObjects(ob ? [ob] : undefined)
}
},
[dataset, setDraggingObject]
[dataset, setDraggingObjects, multiSelect]
)

const onDragOver = useCallback(
Expand All @@ -125,18 +142,18 @@ export function useMove({
const onDragEnd = useCallback(
async (e: DragEndEvent) => {
delayedNavigation(undefined)
setDraggingObject(undefined)
setDraggingObjects(undefined)
moveFiles(e)
},
[setDraggingObject, delayedNavigation, moveFiles]
[setDraggingObjects, delayedNavigation, moveFiles]
)

const onDragCancel = useCallback(
async (e: DragCancelEvent) => {
delayedNavigation(undefined)
setDraggingObject(undefined)
setDraggingObjects(undefined)
},
[setDraggingObject, delayedNavigation]
[setDraggingObjects, delayedNavigation]
)

return {
Expand All @@ -145,6 +162,6 @@ export function useMove({
onDragCancel,
onDragMove,
onDragStart,
draggingObject,
draggingObjects,
}
}
Loading

0 comments on commit 5f1cb4b

Please sign in to comment.