Repository navigation
feat(ui): add bulk retry, cancel and delete to the downloads table - #101
khaosdoctor wants to merge 1 commit into
Conversation
[Medium impact] The change is limited to bulk actions on the Downloads page. Fix bulk Delete reaching rows hidden by live updates before merging.
|
| // Row selection for bulk actions, keyed by download id (see get-row-id) so it | ||
| // survives live updates and paging. | ||
| const rowSelection = ref<Record<string, boolean>>({}) | ||
| const selected = computed(() => (data.value?.downloads ?? []).filter(d => rowSelection.value[String(d.id)])) |
There was a problem hiding this comment.
Delete reaches hidden downloads
selected reads all downloads, not the filtered list. Select a downloading row under the Downloading filter, then let it finish. The live update hides the row without changing the filter, so it stays selected and bulk Delete still removes its record and file. Build the targets from selected rows that still match filtered, while keeping selection across pages.
Knowledge Base Used: Nuxt management dashboard
| return | ||
|
|
||
| bulkPending.value = action | ||
| const results = await Promise.allSettled(targets.map(d => sendAction(d, action))) |
There was a problem hiding this comment.
Deleted downloads leave files behind
Promise.allSettled sends deletes together, but the backend only orders deletes by download ID. Two failed or queued records can share a file. If both deletes check for siblings before either record is removed, each keeps the shared file, then both remove their records and report success. Order deletes that share a path, or make the backend order them by path, so the last delete removes the file.
| // It also drops the selection, so a bulk action never hits rows the filters hide. | ||
| watch([filter, search, peerFilter, codeFilter], () => { | ||
| page.value = 1 | ||
| rowSelection.value = {} |
There was a problem hiding this comment.
Failed rows lose their selection
The new selection reset also runs when keepPresent drops a filter option after a successful action. For example, filter by peers A and B, select their last records, and bulk Delete. If A succeeds and B fails, A disappears from the peer options and this watcher clears B's selection too. Keep failed rows selected when filter options are removed automatically; reserve the full reset for user changes.
| bulkPending.value = action | ||
| const results = await Promise.allSettled(targets.map(d => sendAction(d, action))) | ||
| bulkPending.value = undefined |
There was a problem hiding this comment.
Successful actions show failure messages
runBulkAction bypasses pendingActions, so row buttons remain enabled for downloads already in a bulk request. Bulk actions can also include a row whose own request is still running. A second Delete can return not found after the first succeeds, producing a failure toast for a successful deletion. Use the same per-download pending state in both paths and skip rows already busy.
There was a problem hiding this comment.
🟡 Changes recommended
Bulk actions can delete completed artifacts from selected rows hidden by live filter updates and overlap with in-flight per-row requests, allowing duplicate or conflicting actions.
2 open issues
What changed in this PR
Adds bulk download actions to jack's desktop table and mobile cards, using
the existing per-download endpoints without API changes.
Changes:
- Adds row selection with eligibility counts for Retry, Cancel and Delete.
- Runs requests in parallel, confirms destructive actions, retains failed
selections and clears selection when filters change.
| File | Description |
|---|---|
apps/ui/app/pages/downloads.vue |
Adds selection controls and bulk action handling; shares request logic with per-row actions. |
Review effort: Balanced
| // Row selection for bulk actions, keyed by download id (see get-row-id) so it | ||
| // survives live updates and paging. | ||
| const rowSelection = ref<Record<string, boolean>>({}) | ||
| const selected = computed(() => (data.value?.downloads ?? []).filter(d => rowSelection.value[String(d.id)])) |
| return | ||
|
|
||
| bulkPending.value = action | ||
| const results = await Promise.allSettled(targets.map(d => sendAction(d, action))) |
|
Merged into #102 at the maintainer's request, so it can be reviewed as one PR. |


What does this PR do?
Adds a checkbox column to the Downloads table and a checkbox on each mobile card. Selecting rows shows a bar with Retry, Cancel and Delete, each labelled with how many selected rows it applies to; rows that don't support an action are skipped. Cancel and delete ask for confirmation, like the per-row buttons.
Related issue
None.
Checklist
mise run lint:fixwas run and its output is committedmise run testpasses (andmise run test:e2eif transfer/request paths changed)