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 Nov 20, 2024
1 parent 08c2c06 commit 9d8b9d7
Show file tree
Hide file tree
Showing 17 changed files with 574 additions and 148 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-hairs-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/e2e': minor
---

Added methods for mouse move and hover behaviours.
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 via drag and drop or the multi-select actions menu. This works even when selecting files (and entire directories) from across multiple different origin directories.
5 changes: 5 additions & 0 deletions .changeset/real-lemons-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

The table now supports multiple dragging datums.
4 changes: 2 additions & 2 deletions apps/renterd-e2e/src/specs/files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ test('batch delete across nested directories', async ({ page }) => {
await file3.click()
const file4 = await getFileRowById(page, 'bucket1/dir2/file4.txt')
await file4.click()
const menu = page.getByLabel('file multiselect menu')
const menu = page.getByLabel('file multi-select menu')

// Delete selected files.
await menu.getByLabel('delete selected files').click()
Expand Down Expand Up @@ -295,7 +295,7 @@ test('batch delete using the all files explorer mode', async ({ page }) => {
await file3.click()
const file4 = await getFileRowById(page, 'bucket1/dir2/file4.txt')
await file4.click()
const menu = page.getByLabel('file multiselect menu')
const menu = page.getByLabel('file multi-select menu')

// Delete selected files.
await menu.getByLabel('delete selected files').click()
Expand Down
195 changes: 195 additions & 0 deletions apps/renterd-e2e/src/specs/filesMove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { test } from '@playwright/test'
import { navigateToBuckets } from '../fixtures/navigate'
import { createBucket, openBucket } from '../fixtures/buckets'
import {
getFileRowById,
openDirectory,
createFilesMap,
expectFilesMap,
navigateToParentDirectory,
} 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,
}) => {
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,
dir3: {
'file5.txt': null,
'file6.txt': null,
},
},
})
await navigateToBuckets({ page })
await openBucket(page, bucketName)

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

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

// Move all selected files by dragging one of them.
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',
dir3: {
'file5.txt': 'visible',
'file6.txt': 'visible',
},
dir1: {
'file2.txt': 'visible',
},
dir2: {
'file3.txt': 'hidden',
'file4.txt': 'visible',
dir3: 'hidden',
},
})
})

test('move a file via drag and drop while leaving a separate set of selected files in place', async ({
page,
}) => {
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
await createFilesMap(page, bucketName, {
'file0.txt': null,
'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()

// Move file5 which is not in the selection.
const file5 = await getFileRowById(page, 'bucket1/dir2/file5.txt', true)
await moveMouseOver(page, file5)
await page.mouse.down()

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

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

await expectFilesMap(page, bucketName, {
'file0.txt': 'visible',
'file1.txt': 'visible',
'file5.txt': 'visible',
dir1: {
'file2.txt': 'visible',
},
dir2: {
'file3.txt': 'visible',
'file4.txt': 'visible',
},
})
})

test('move files by selecting and using the docked menu batch action', async ({
page,
}) => {
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,
dir3: {
'file5.txt': null,
'file6.txt': null,
},
},
})
await navigateToBuckets({ page })
await openBucket(page, bucketName)

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

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

await navigateToParentDirectory(page)

const menu = page.getByLabel('file multi-select menu')

// Delete selected files.
await menu.getByLabel('move selected files to the current directory').click()
const dialog = page.getByRole('dialog')
await dialog.getByRole('button', { name: 'Move' }).click()

await expectFilesMap(page, bucketName, {
'file1.txt': 'visible',
'file3.txt': 'visible',
dir3: {
'file5.txt': 'visible',
'file6.txt': 'visible',
},
dir1: {
'file2.txt': 'visible',
},
dir2: {
'file3.txt': 'hidden',
'file4.txt': 'visible',
dir3: 'hidden',
},
})
})
2 changes: 1 addition & 1 deletion apps/renterd-e2e/src/specs/keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ test('batch delete multiple keys', async ({ page }) => {
await rowIdx3.click({ modifiers: ['Shift'] })

// Delete all 4 keys.
const menu = page.getByLabel('key multiselect menu')
const menu = page.getByLabel('key multi-select menu')
await menu.getByLabel('delete selected keys').click()
const dialog = page.getByRole('dialog')
await dialog.getByRole('button', { name: 'Delete' }).click()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { FolderMoveTo16 } from '@siafoundation/react-icons'
import { useFilesDirectory } from '../../../contexts/filesDirectory'
import { useDialog } from '../../../contexts/dialog'

export function FilesBatchMove() {
const { openConfirmDialog } = useDialog()
const { multiSelect, moveSelectedFiles, moveSelectedFilesOperationCount } =
useFilesDirectory()

return (
<Button
disabled={moveSelectedFilesOperationCount === 0}
aria-label="move selected files to the current directory"
tip="Move selected files to the current directory"
onClick={() => {
openConfirmDialog({
title: `Move files`,
action: 'Move',
variant: 'accent',
body: (
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to move the{' '}
{multiSelect.selectionCount.toLocaleString()} selected files to
the current directory?
</Paragraph>
</div>
),
onConfirm: async () => {
moveSelectedFiles()
},
})
}}
>
<FolderMoveTo16 />
</Button>
)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { MultiSelectionMenu } from '@siafoundation/design-system'
import { FilesBatchDelete } from '../../Files/batchActions/FilesBatchDelete'
import { useFilesDirectory } from '../../../contexts/filesDirectory'
import { FilesBatchMove } from './FilesBatchMove'

export function FilesDirectoryBatchMenu() {
const { multiSelect } = useFilesDirectory()

return (
<MultiSelectionMenu multiSelect={multiSelect} entityWord="file">
<FilesBatchMove />
<FilesBatchDelete multiSelect={multiSelect} />
</MultiSelectionMenu>
)
Expand Down
6 changes: 4 additions & 2 deletions apps/renterd/components/FilesDirectory/FilesExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EmptyState } from './EmptyState'
import { useCanUpload } from '../Files/useCanUpload'
import { useFilesManager } from '../../contexts/filesManager'
import { columns } from '../../contexts/filesDirectory/columns'
import { pluralize } from '@siafoundation/units'

export function FilesExplorer() {
const {
Expand All @@ -24,7 +25,7 @@ export function FilesExplorer() {
onDragStart,
onDragCancel,
onDragMove,
draggingObject,
draggingObjects,
} = useFilesDirectory()
const canUpload = useCanUpload()
return (
Expand Down Expand Up @@ -53,7 +54,8 @@ export function FilesExplorer() {
onDragEnd={onDragEnd}
onDragCancel={onDragCancel}
onDragMove={onDragMove}
draggingDatum={draggingObject}
draggingDatums={draggingObjects}
draggingMultipleLabel={(count) => `move ${pluralize(count, 'file')}`}
/>
</Dropzone>
</div>
Expand Down
41 changes: 23 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,23 @@ function useFilesDirectoryMain() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeBucket])

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

const datasetPageWithOnClick = useMemo(() => {
if (!_datasetPage) {
return undefined
Expand Down Expand Up @@ -106,8 +109,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 +123,7 @@ function useFilesDirectoryMain() {
isDraggable: d.type !== 'bucket' && !d.isUploading,
}
})
}, [datasetPageWithOnClick, draggingObject])
}, [datasetPageWithOnClick, draggingObjects])

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

Expand Down
Loading

0 comments on commit 9d8b9d7

Please sign in to comment.