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 23995b8
Show file tree
Hide file tree
Showing 24 changed files with 591 additions and 162 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.
2 changes: 1 addition & 1 deletion .changeset/early-toys-know.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'renterd': minor
---

The directory-based file explorer now supports multiselect across any files and directories.
The directory-based file explorer now supports multi-select across any files and directories.
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.
2 changes: 1 addition & 1 deletion .changeset/great-points-hide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@siafoundation/design-system': minor
---

Added useMultiSelect hook that tracks multiselect state. It supports selection, shift-selecting for ranges, deselection, and works across pagination.
Added useMultiSelect hook that tracks multi-select state. It supports selection, shift-selecting for ranges, deselection, and works across pagination.
2 changes: 1 addition & 1 deletion .changeset/happy-comics-retire.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'renterd': minor
---

The key management table now supports multiselect and batch deletion.
The key management table now supports multi-select and batch deletion.
2 changes: 1 addition & 1 deletion .changeset/lazy-pandas-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'renterd': minor
---

The "all files" file explorer now supports multiselect across any files.
The "all files" file explorer now supports multi-select across any files.
2 changes: 1 addition & 1 deletion .changeset/olive-cougars-divide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'renterd': minor
---

The "all files" file explorer multiselect menu now supports batch deletion of selected files.
The "all files" file explorer multi-select menu now supports batch deletion of selected files.
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.
2 changes: 1 addition & 1 deletion .changeset/two-seas-shake.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'renterd': minor
---

The directory-based file explorer multiselect menu now supports batch deletion of selected files and directories.
The directory-based file explorer multi-select menu now supports batch deletion of selected files and directories.
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 @@ -204,7 +204,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 @@ -250,7 +250,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
196 changes: 196 additions & 0 deletions apps/renterd-e2e/src/specs/filesMove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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,
}) => {
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,
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,
}) => {
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()

// 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 moveMouseOver(page, file1)
await page.mouse.up()

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

test('move files by selecting and using the docked menu batch action', 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,
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
10 changes: 3 additions & 7 deletions apps/renterd/components/Files/batchActions/FilesBatchDelete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ import {
Paragraph,
triggerSuccessToast,
triggerErrorToast,
MultiSelect,
} from '@siafoundation/design-system'
import { Delete16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useObjectsRemove } from '@siafoundation/renterd-react'
import { ObjectData } from '../../../contexts/filesManager/types'
import { useFilesDirectory } from '../../../contexts/filesDirectory'

export function FilesBatchDelete({
multiSelect,
}: {
multiSelect: MultiSelect<ObjectData>
}) {
export function FilesBatchDelete() {
const { multiSelect } = useFilesDirectory()
const filesToDelete = useMemo(
() =>
Object.entries(multiSelect.selectionMap).map(([_, item]) => ({
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,13 +1,15 @@
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">
<FilesBatchDelete multiSelect={multiSelect} />
<FilesBatchMove />
<FilesBatchDelete />
</MultiSelectionMenu>
)
}
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
Loading

0 comments on commit 23995b8

Please sign in to comment.