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 13, 2024
1 parent 5eec9a5 commit 1417b47
Show file tree
Hide file tree
Showing 19 changed files with 579 additions and 157 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 @@ -255,7 +255,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 @@ -300,7 +300,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
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function FilesFlatBatchMenu() {

return (
<MultiSelectionMenu multiSelect={multiSelect} entityWord="file">
<FilesBatchDelete multiSelect={multiSelect} />
<FilesBatchDelete />
</MultiSelectionMenu>
)
}
Loading

0 comments on commit 1417b47

Please sign in to comment.