[PM-37808] Bulk Selection + Batch Bar for Desktop#21308
[PM-37808] Bulk Selection + Batch Bar for Desktop#21308nick-livefront wants to merge 15 commits into
Conversation
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the desktop bulk-selection batch bar wiring behind the Code Review DetailsNo new findings. The two previously posted findings (collections silently dropped; British spelling) were addressed by the author, and the remaining open threads reflect intentional desktop behavior (cipher-only "select all", local-state cleanup after backend deletion) confirmed in discussion. Notes considered and intentionally not raised as findings:
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #21308 +/- ##
==========================================
- Coverage 49.32% 49.28% -0.04%
==========================================
Files 4096 4098 +2
Lines 128869 128974 +105
Branches 19776 19814 +38
==========================================
+ Hits 63567 63568 +1
- Misses 60592 60692 +100
- Partials 4710 4714 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jaasen-livefront
left a comment
There was a problem hiding this comment.
Looks great! Just a couple comments. ;)
| if (this.collections.length) { | ||
| const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); | ||
| await this.collectionService.delete( | ||
| this.collections.map((c) => c.id as CollectionId), | ||
| userId, | ||
| ); | ||
| this.toastService.showToast({ | ||
| variant: "success", | ||
| message: this.i18nService.t("deletedCollections"), | ||
| }); | ||
| } |
There was a problem hiding this comment.
❓ Is this necessary since we're already awaiting the deleteCollections above?
There was a problem hiding this comment.
Yes it is, a tad bit confusing. It's identical to the web implementation here. The first set of promises is deleting them from the backend and this block here is removing them from the local state.
| type="checkbox" | ||
| bitCheckbox | ||
| [disabled]="disabled() || isEmpty()" | ||
| (change)="$event ? toggleAll() : null" |
There was a problem hiding this comment.
🎨 I believe $event is always truthy so this check shouldn't be required.
| return cipherItems.length > 0 && cipherItems.every((i) => this.selection.isSelected(i)); | ||
| } | ||
|
|
||
| protected toggleAll(): void { |
There was a problem hiding this comment.
❓ Just to verify we only want to select ciphers here not collections?
There was a problem hiding this comment.
Correct, on the desktop there isn't a view that shows you only collections like there is on the web vault. This is more defensive than anything.
There was a problem hiding this comment.
I think we can use the opportunity to align to the new dialog designs. I already started with this for the web e1868e7
There was a problem hiding this comment.
Added e7fd8c2. You have BulkDeleteService in the apps/web, could it be moved to libs/vault so that I could reuse that as well?
There was a problem hiding this comment.
Yeah that makes sense
| protected readonly submit = async () => { | ||
| const deletePromises: Promise<void>[] = []; | ||
|
|
||
| if (this.unassignedCiphers.length && this.organization?.canEditUnassignedCiphers) { | ||
| deletePromises.push(this.deleteCiphersAdmin(this.unassignedCiphers)); | ||
| } | ||
| if (this.cipherIds.length) { | ||
| if (!this.organization || !this.organization.canEditAllCiphers) { | ||
| deletePromises.push(this.deleteCiphers()); | ||
| } else { | ||
| deletePromises.push(this.deleteCiphersAdmin(this.cipherIds)); | ||
| } | ||
| } | ||
|
|
||
| await Promise.all(deletePromises); | ||
|
|
||
| if (this.totalCiphersCount) { | ||
| this.toastService.showToast({ | ||
| variant: "success", | ||
| message: this.i18nService.t(this.permanent ? "permanentlyDeletedItems" : "deletedItems"), | ||
| }); | ||
| } | ||
| this.close(BulkDeleteDialogResult.Deleted); | ||
| }; |
There was a problem hiding this comment.
❌ CRITICAL: Selected collections are silently dropped — the dialog reports success but never deletes them.
Details and fix
VaultBatchBarService.bulkDelete() passes collections into this dialog (bulk-delete-dialog.token.ts includes collections?: CollectionView[]), and collection rows are individually selectable on desktop (vault-list.component.html wires (checkboxChange)="selection.toggle(item)" on collection rows, and collectionsToDisplay is populated into the data source). canDelete() evaluates collections too, so the delete action is offered when a collection is selected.
However, this component only handles cipherIds / unassignedCiphers. The collections param is never read or deleted. Result:
- Select a collection (with or without ciphers) → click delete →
submitruns, ignores collections, returnsBulkDeleteDialogResult.Deleted, selection clears and the list refreshes. - If only a collection is selected,
totalCiphersCount === 0, so no toast even fires — the user sees the row vanish from the refreshed list region with no error, but the collection still exists on the server.
The web equivalent (apps/web/.../bulk-delete-dialog/bulk-delete-dialog.component.ts) deletes collections via deleteCollections(). The desktop app already imports CollectionService (vault.component.ts:177), so this dialog can implement the same path — read this.dialogData.collections, delete via CollectionService/ApiService.deleteManyCollections, and surface the deletedCollections / collectionsDeleted toast (keys were added in this PR but are currently unused).
Alternatively, if desktop intentionally does not support collection deletion yet, prevent collections from being selectable on desktop so the delete action is never offered for them.
There was a problem hiding this comment.
Deletion of collections is not available on the desktop but this was resolved from 267bd0f which implements the same behavior as the web
|
@gbubemismith I merged up with main and resolved some of the translation work that you did your branch. |
| "copyPrivateKey": { | ||
| "message": "Copy private key" | ||
| }, | ||
| "copyPublicKey": { | ||
| "message": "Copy public key" | ||
| }, | ||
| "copyFingerprint": { | ||
| "message": "Copy fingerprint" | ||
| }, |
There was a problem hiding this comment.
These were duplicate keys in the file
…08/desktop-vault-export
| "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." | ||
| }, | ||
| "personalItemsTransferWarningPlural": { | ||
| "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organisation. You will no longer own these items.", |
There was a problem hiding this comment.
🎨 SUGGESTED: British spelling "organisation" is inconsistent with the rest of the en-US locale.
Details and fix
Every other string in this file (and the matching personalItemsTransferWarningPlural key in apps/web/src/locales/en/messages.json:12318) uses the American spelling "organization". This is the only "organisation" in the file.
| "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organisation. You will no longer own these items.", | |
| "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", |
This keeps the desktop copy in sync with the web copy for the same key.
|
@gbubemismith - I addressed Claude's comment: #21308 (comment) |
|



🎟️ Tracking
PM-37808
📔 Objective
Wires up the vault batch bar (multi-select) for the desktop vault behind the PM37785_VaultBatchBar and PM37785_DesktopVaultBatchBar feature flags.
Includes a desktop-specific
BulkDeleteDialogDesktopComponentbecause the collection deletion logic importsCollectionServicefrom@bitwarden/admin-console, whichlibs/vaultcannot depend on. This prevents the web dialog from being refactored into a shared location, so each app provides its own implementation via theBULK_DELETE_DIALOGinjection token.📸 Screenshots
Screen.Recording.2026-06-24.at.1.18.56.PM.mov