diff --git a/.changeset/perfect-houses-approve.md b/.changeset/perfect-houses-approve.md
new file mode 100644
index 000000000..16b519d28
--- /dev/null
+++ b/.changeset/perfect-houses-approve.md
@@ -0,0 +1,5 @@
+---
+'@siafoundation/design-system': minor
+---
+
+Add handleBatchOperation method.
diff --git a/.changeset/shiny-gifts-hope.md b/.changeset/shiny-gifts-hope.md
new file mode 100644
index 000000000..e1a61e25c
--- /dev/null
+++ b/.changeset/shiny-gifts-hope.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The contracts multi-select menu now supports bulk deletion.
diff --git a/apps/renterd-e2e/src/specs/contracts.spec.ts b/apps/renterd-e2e/src/specs/contracts.spec.ts
index 4c7a93f7f..984c7f44d 100644
--- a/apps/renterd-e2e/src/specs/contracts.spec.ts
+++ b/apps/renterd-e2e/src/specs/contracts.spec.ts
@@ -61,3 +61,21 @@ test('contracts prunable size', async ({ page }) => {
await expect(prunableSize).toBeVisible()
}
})
+
+test('bulk delete contracts', async ({ page }) => {
+ await navigateToContracts({ page })
+ const rows = await getContractRows(page)
+ for (const row of rows) {
+ await row.click()
+ }
+
+ // Delete selected contracts.
+ const menu = page.getByLabel('contract multi-select menu')
+ await menu.getByLabel('delete selected contracts').click()
+ const dialog = page.getByRole('dialog')
+ await dialog.getByRole('button', { name: 'Delete' }).click()
+
+ await expect(
+ page.getByText('There are currently no active contracts')
+ ).toBeVisible()
+})
diff --git a/apps/renterd-e2e/src/specs/files.spec.ts b/apps/renterd-e2e/src/specs/files.spec.ts
index 0954dcf0c..d61e53491 100644
--- a/apps/renterd-e2e/src/specs/files.spec.ts
+++ b/apps/renterd-e2e/src/specs/files.spec.ts
@@ -222,7 +222,7 @@ test('shows a new intermediate directory when uploading nested files', async ({
await deleteBucket(page, bucketName)
})
-test('batch delete across nested directories', async ({ page }) => {
+test('bulk delete across nested directories', async ({ page }) => {
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
@@ -267,7 +267,7 @@ test('batch delete across nested directories', async ({ page }) => {
})
})
-test('batch delete using the all files explorer mode', async ({ page }) => {
+test('bulk delete using the all files explorer mode', async ({ page }) => {
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
diff --git a/apps/renterd-e2e/src/specs/filesMove.spec.ts b/apps/renterd-e2e/src/specs/filesMove.spec.ts
index 1582e2305..b0d5eb0b0 100644
--- a/apps/renterd-e2e/src/specs/filesMove.spec.ts
+++ b/apps/renterd-e2e/src/specs/filesMove.spec.ts
@@ -136,7 +136,7 @@ test('move a file via drag and drop while leaving a separate set of selected fil
})
})
-test('move files by selecting and using the docked menu batch action', async ({
+test('move files by selecting and using the docked menu bulk action', async ({
page,
}) => {
const bucketName = 'bucket1'
diff --git a/apps/renterd-e2e/src/specs/keys.spec.ts b/apps/renterd-e2e/src/specs/keys.spec.ts
index cc73c804a..22167d6ff 100644
--- a/apps/renterd-e2e/src/specs/keys.spec.ts
+++ b/apps/renterd-e2e/src/specs/keys.spec.ts
@@ -25,7 +25,7 @@ test('create and delete a key', async ({ page }) => {
await expect(row).toBeHidden()
})
-test('batch delete multiple keys', async ({ page }) => {
+test('bulk delete multiple keys', async ({ page }) => {
// Create 3 keys. Note: 1 already exists.
const key1 = await createKey(page)
const key2 = await createKey(page)
diff --git a/apps/renterd/CHANGELOG.md b/apps/renterd/CHANGELOG.md
index 1564dfde5..101cfe6a8 100644
--- a/apps/renterd/CHANGELOG.md
+++ b/apps/renterd/CHANGELOG.md
@@ -12,13 +12,13 @@
- 17b29cf3: Navigating into a directory in the file explorer is now by clicking on the directory name rather than anywhere on the row.
- 17b29cf3: The directory-based file explorer now supports multiselect across any files and directories.
-- 6c7e3681: The key management table now supports multiselect and batch deletion.
+- 6c7e3681: The key management table now supports multiselect and bulk deletion.
- 17b29cf3: The "all files" file explorer now supports multiselect across any files.
-- 17b29cf3: The "all files" file explorer multiselect menu now supports batch deletion of selected files.
+- 17b29cf3: The "all files" file explorer multiselect menu now supports bulk deletion of selected files.
- 6c7e3681: The onboarding wizard now animates in and out.
- ed264a0d: The transfers bar now animates in and out.
- 09142864: The keys table now has pagination controls.
-- 17b29cf3: The directory-based file explorer multiselect menu now supports batch deletion of selected files and directories.
+- 17b29cf3: The directory-based file explorer multiselect menu now supports bulk deletion of selected files and directories.
### Patch Changes
diff --git a/apps/renterd/components/Contracts/ContractsBatchMenu/ContractsBatchDelete.tsx b/apps/renterd/components/Contracts/ContractsBatchMenu/ContractsBatchDelete.tsx
new file mode 100644
index 000000000..66472c297
--- /dev/null
+++ b/apps/renterd/components/Contracts/ContractsBatchMenu/ContractsBatchDelete.tsx
@@ -0,0 +1,66 @@
+import {
+ Button,
+ handleBatchOperation,
+ Paragraph,
+} from '@siafoundation/design-system'
+import { Delete16 } from '@siafoundation/react-icons'
+import { useContractDelete } from '@siafoundation/renterd-react'
+import { useCallback, useMemo } from 'react'
+import { useDialog } from '../../../contexts/dialog'
+import { useContracts } from '../../../contexts/contracts'
+import { pluralize } from '@siafoundation/units'
+
+export function ContractsBatchDelete() {
+ const { multiSelect } = useContracts()
+
+ const ids = useMemo(
+ () => Object.entries(multiSelect.selectionMap).map(([_, item]) => item.id),
+ [multiSelect.selectionMap]
+ )
+ const { openConfirmDialog } = useDialog()
+ const deleteContract = useContractDelete()
+ const deleteAll = useCallback(async () => {
+ await handleBatchOperation(
+ ids.map((id) => deleteContract.delete({ params: { id } })),
+ {
+ toastError: ({ successCount, errorCount, totalCount }) => ({
+ title: `${pluralize(successCount, 'contract')} deleted`,
+ body: `Error deleting ${errorCount}/${totalCount} total contracts.`,
+ }),
+ toastSuccess: ({ totalCount }) => ({
+ title: `${pluralize(totalCount, 'contract')} deleted`,
+ }),
+ after: () => {
+ multiSelect.deselectAll()
+ },
+ }
+ )
+ }, [multiSelect, ids, deleteContract])
+
+ return (
+
+ )
+}
diff --git a/apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx b/apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx
new file mode 100644
index 000000000..e45343bf8
--- /dev/null
+++ b/apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx
@@ -0,0 +1,13 @@
+import { MultiSelectionMenu } from '@siafoundation/design-system'
+import { useContracts } from '../../../contexts/contracts'
+import { ContractsBatchDelete } from './ContractsBatchDelete'
+
+export function ContractsBatchMenu() {
+ const { multiSelect } = useContracts()
+
+ return (
+