From 323f30c8e941d3bfb72acb812e6a90e949771a5d Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:36:39 -0800 Subject: [PATCH] [PM-29892] - fix bulk share in vault (#18601) * fix bulk share in vault * clean up types. * remove unnecessary optional chain * add back defensive programming. update restore * fix searchableCollectionNodes * add back optional chains --- .../collections/vault.component.ts | 14 +- .../vault-item-dialog.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 145 +++++++++--------- 3 files changed, 81 insertions(+), 80 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 073d73f6a50..a641116f4de 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -472,7 +472,7 @@ export class VaultComponent implements OnInit, OnDestroy { collections, filter.collectionId, ); - searchableCollectionNodes = selectedCollection.children ?? []; + searchableCollectionNodes = selectedCollection?.children ?? []; } let collectionsToReturn: CollectionAdminView[] = []; @@ -962,10 +962,10 @@ export class VaultComponent implements OnInit, OnDestroy { await this.editCipher(cipher, true); } - restore = async (c: CipherViewLike): Promise => { + restore = async (c: CipherViewLike): Promise => { const organization = await firstValueFrom(this.organization$); if (!CipherViewLikeUtils.isDeleted(c)) { - return false; + return; } if ( @@ -974,11 +974,11 @@ export class VaultComponent implements OnInit, OnDestroy { !organization.allowAdminAccessToAllCollectionItems ) { this.showMissingPermissionsError(); - return false; + return; } if (!(await this.repromptCipher([c]))) { - return false; + return; } // Allow restore of an Unassigned Item @@ -996,10 +996,10 @@ export class VaultComponent implements OnInit, OnDestroy { message: this.i18nService.t("restoredItem"), }); this.refresh(); - return true; + return; } catch (e) { this.logService.error(e); - return false; + return; } }; diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 2340f74c32d..4c6efdee167 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -100,7 +100,7 @@ export interface VaultItemDialogParams { /** * Function to restore a cipher from the trash. */ - restore?: (c: CipherViewLike) => Promise; + restore?: (c: CipherViewLike) => Promise; } export const VaultItemDialogResult = { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 6fbe3f08912..5ff72b0d147 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,15 +1,6 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, viewChild } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - firstValueFrom, - lastValueFrom, - Observable, - Subject, -} from "rxjs"; +import { combineLatest, firstValueFrom, lastValueFrom, Observable, of, Subject } from "rxjs"; import { concatMap, debounceTime, @@ -18,6 +9,7 @@ import { first, map, shareReplay, + startWith, switchMap, take, takeUntil, @@ -89,7 +81,6 @@ import { CipherListView } from "@bitwarden/sdk-internal"; import { AddEditFolderDialogComponent, AddEditFolderDialogResult, - AttachmentDialogCloseResult, AttachmentDialogResult, AttachmentsV2Component, CipherFormConfig, @@ -179,14 +170,10 @@ type EmptyStateMap = Record; ], }) export class VaultComponent implements OnInit, OnDestroy { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; + readonly filterComponent = viewChild(VaultFilterComponent); + readonly vaultItemsComponent = viewChild(VaultItemsComponent); - trashCleanupWarning: string = null; + trashCleanupWarning: string = ""; activeFilter: VaultFilter = new VaultFilter(); protected deactivatedOrgIcon = DeactivatedOrg; @@ -198,20 +185,20 @@ export class VaultComponent implements OnInit, OnDestr protected refreshing = false; protected processingEvent = false; protected filter: RoutedVaultFilterModel = {}; - protected showBulkMove: boolean; - protected canAccessPremium: boolean; - protected allCollections: CollectionView[]; + protected showBulkMove: boolean = false; + protected canAccessPremium: boolean = false; + protected allCollections: CollectionView[] = []; protected allOrganizations: Organization[] = []; - protected ciphers: C[]; - protected collections: CollectionView[]; - protected isEmpty: boolean; + protected ciphers: C[] = []; + protected collections: CollectionView[] = []; + protected isEmpty: boolean = false; protected selectedCollection: TreeNode | undefined; protected canCreateCollections = false; protected currentSearchText$: Observable = this.route.queryParams.pipe( map((queryParams) => queryParams.search), ); private searchText$ = new Subject(); - private refresh$ = new BehaviorSubject(null); + private refresh$ = new Subject(); private destroy$ = new Subject(); private vaultItemDialogRef?: DialogRef | undefined; @@ -220,7 +207,7 @@ export class VaultComponent implements OnInit, OnDestr organizations$ = this.accountService.activeAccount$ .pipe(map((a) => a?.id)) - .pipe(switchMap((id) => this.organizationService.organizations$(id))); + .pipe(switchMap((id) => (id ? this.organizationService.organizations$(id) : of([])))); emptyState$ = combineLatest([ this.currentSearchText$, @@ -228,7 +215,7 @@ export class VaultComponent implements OnInit, OnDestr this.organizations$, ]).pipe( map(([searchText, filter, organizations]) => { - const selectedOrg = organizations?.find((org) => org.id === filter.organizationId); + const selectedOrg = organizations.find((org) => org.id === filter.organizationId); const isOrgDisabled = selectedOrg && !selectedOrg.enabled; if (isOrgDisabled) { @@ -586,7 +573,7 @@ export class VaultComponent implements OnInit, OnDestr firstSetup$ .pipe( - switchMap(() => this.refresh$), + switchMap(() => this.refresh$.pipe(startWith(undefined))), tap(() => (this.refreshing = true)), switchMap(() => combineLatest([ @@ -712,7 +699,6 @@ export class VaultComponent implements OnInit, OnDestr async handleUnknownCipher() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("unknownCipher"), }); await this.router.navigate([], { @@ -842,9 +828,13 @@ export class VaultComponent implements OnInit, OnDestr if (orgId == null) { orgId = "MyVault"; } - const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$); + const data = this.filterComponent()?.filters?.organizationFilter?.data$; + if (data == undefined) { + return; + } + const orgs = await firstValueFrom(data); const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode; - await this.filterComponent.filters?.organizationFilter?.action(orgNode); + await this.filterComponent()?.filters?.organizationFilter?.action(orgNode); } addFolder = (): void => { @@ -912,7 +902,10 @@ export class VaultComponent implements OnInit, OnDestr canEditCipher: cipher.edit, }); - const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed); + const result = await lastValueFrom(dialogRef.closed); + if (result === undefined) { + return; + } if ( result.action === AttachmentDialogResult.Uploaded || @@ -966,7 +959,7 @@ export class VaultComponent implements OnInit, OnDestr */ async addCipher(cipherType?: CipherType) { const type = cipherType ?? this.activeFilter.cipherType; - const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type); + const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", undefined, type); const collectionId = this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null ? this.activeFilter.collectionId @@ -994,7 +987,7 @@ export class VaultComponent implements OnInit, OnDestr } async editCipher(cipher: CipherView | CipherListView, cloneMode?: boolean) { - return this.editCipherId(uuidAsString(cipher?.id), cloneMode); + return this.editCipherId(uuidAsString(cipher.id), cloneMode); } /** @@ -1088,6 +1081,9 @@ export class VaultComponent implements OnInit, OnDestr }, }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } if (result.action === CollectionDialogAction.Saved) { if (result.collection) { // Update CollectionService with the new collection @@ -1104,7 +1100,7 @@ export class VaultComponent implements OnInit, OnDestr async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise { const dialog = openCollectionDialog(this.dialogService, { data: { - collectionId: c?.id, + collectionId: c.id, organizationId: c.organizationId, initialTab: tab, limitNestedCollections: true, @@ -1112,6 +1108,9 @@ export class VaultComponent implements OnInit, OnDestr }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (result.action === CollectionDialogAction.Saved) { if (result.collection) { @@ -1163,7 +1162,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("deletedCollectionId", collection.name), }); if (navigateAway) { @@ -1196,12 +1194,12 @@ export class VaultComponent implements OnInit, OnDestr let availableCollections: CollectionView[] = []; const orgId = this.activeFilter.organizationId || - ciphers.find((c) => c.organizationId !== null)?.organizationId; + ciphers.find((c) => c.organizationId !== undefined)?.organizationId; if (orgId && orgId !== "MyVault") { const organization = this.allOrganizations.find((o) => o.id === orgId); availableCollections = this.allCollections.filter( - (c) => c.organizationId === organization.id, + (c) => c.organizationId === organization?.id, ); } @@ -1229,7 +1227,7 @@ export class VaultComponent implements OnInit, OnDestr ciphers: ciphersToAssign, organizationId: orgId as OrganizationId, availableCollections, - activeCollection: this.activeFilter?.selectedCollectionNode?.node, + activeCollection: this.activeFilter.selectedCollectionNode?.node, }, }); @@ -1255,7 +1253,7 @@ export class VaultComponent implements OnInit, OnDestr await this.editCipher(cipher, true); } - restore = async (c: C): Promise => { + restore = async (c: CipherViewLike) => { let toastMessage; if (!CipherViewLikeUtils.isDeleted(c)) { return; @@ -1281,13 +1279,14 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } catch (e) { this.logService.error(e); + return; } + return; }; async bulkRestore(ciphers: C[]) { @@ -1311,7 +1310,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1321,23 +1319,24 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreManyWithServer(selectedCipherIds, activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } private async handleDeleteEvent(items: VaultItem[]) { - const ciphers: C[] = items.filter((i) => i.collection === undefined).map((i) => i.cipher); - const collections = items.filter((i) => i.cipher === undefined).map((i) => i.collection); + const ciphers = items + .filter((i) => i.collection === undefined && i.cipher !== undefined) + .map((i) => i.cipher as C); + const collections = items + .filter((i) => i.collection !== undefined) + .map((i) => i.collection as CollectionView); if (ciphers.length === 1 && collections.length === 0) { await this.deleteCipher(ciphers[0]); } else if (ciphers.length === 0 && collections.length === 1) { await this.deleteCollection(collections[0]); } else { - const orgIds = items - .filter((i) => i.cipher === undefined) - .map((i) => i.collection.organizationId); + const orgIds = collections.map((c) => c.organizationId); const orgs = await firstValueFrom( this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))), ); @@ -1345,7 +1344,7 @@ export class VaultComponent implements OnInit, OnDestr } } - async deleteCipher(c: C): Promise { + async deleteCipher(c: C) { if (!(await this.repromptCipher([c]))) { return; } @@ -1364,7 +1363,7 @@ export class VaultComponent implements OnInit, OnDestr }); if (!confirmed) { - return false; + return; } try { @@ -1373,7 +1372,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"), }); this.refresh(); @@ -1390,7 +1388,6 @@ export class VaultComponent implements OnInit, OnDestr if (ciphers.length === 0 && collections.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1430,7 +1427,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1454,11 +1450,8 @@ export class VaultComponent implements OnInit, OnDestr const login = CipherViewLikeUtils.getLogin(cipher); if (!login) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); + return; } if (field === "username") { @@ -1471,15 +1464,15 @@ export class VaultComponent implements OnInit, OnDestr typeI18nKey = "password"; } else if (field === "totp") { aType = "TOTP"; + if (!login.totp) { + this.showErrorToast(); + return; + } const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp)); value = totpResponse.code; typeI18nKey = "verificationCodeTotp"; } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); return; } @@ -1494,10 +1487,13 @@ export class VaultComponent implements OnInit, OnDestr return; } + if (!value) { + this.showErrorToast(); + return; + } this.platformUtilsService.copyToClipboard(value, { window: window }); this.toastService.showToast({ variant: "info", - title: null, message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), }); @@ -1514,6 +1510,13 @@ export class VaultComponent implements OnInit, OnDestr } } + showErrorToast() { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("unexpectedError"), + }); + } + /** * Toggles the favorite status of the cipher and updates it on the server. */ @@ -1525,7 +1528,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t( cipherFullView.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites", ), @@ -1540,15 +1542,15 @@ export class VaultComponent implements OnInit, OnDestr : this.cipherService.softDeleteWithServer(id, userId); } - protected async repromptCipher(ciphers: C[]) { + protected async repromptCipher(ciphers: CipherViewLike[]) { const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); } private refresh() { - this.refresh$.next(); - this.vaultItemsComponent?.clearSelection(); + this.refresh$.next(undefined); + this.vaultItemsComponent()?.clearSelection(); } private async go(queryParams: any = null) { @@ -1573,7 +1575,6 @@ export class VaultComponent implements OnInit, OnDestr private showMissingPermissionsError() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("missingPermissions"), }); } @@ -1584,13 +1585,13 @@ export class VaultComponent implements OnInit, OnDestr */ private async getPasswordFromCipherViewLike(cipher: C): Promise { if (!CipherViewLikeUtils.isCipherListView(cipher)) { - return Promise.resolve(cipher.login?.password); + return Promise.resolve(cipher?.login?.password); } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const _cipher = await this.cipherService.get(uuidAsString(cipher.id), activeUserId); const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); - return cipherView.login?.password; + return cipherView.login.password; } }