From 1d6f0bd7dbd445163a38b783aaccb709fb3a3fc5 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Tue, 27 Jan 2026 09:56:06 -0800 Subject: [PATCH] fix bulk share in vault --- .../vault/individual-vault/vault.component.ts | 144 ++++++++++-------- 1 file changed, 81 insertions(+), 63 deletions(-) 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 532757852a3..47d8553047c 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,6 +1,4 @@ -// 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, @@ -8,6 +6,7 @@ import { firstValueFrom, lastValueFrom, Observable, + of, Subject, zip, } from "rxjs"; @@ -93,7 +92,6 @@ import { CipherListView } from "@bitwarden/sdk-internal"; import { AddEditFolderDialogComponent, AddEditFolderDialogResult, - AttachmentDialogCloseResult, AttachmentDialogResult, AttachmentsV2Component, CipherFormConfig, @@ -187,15 +185,11 @@ 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; - kdfIterations: number; + trashCleanupWarning: string | null = null; + kdfIterations: number | null = null; activeFilter: VaultFilter = new VaultFilter(); protected deactivatedOrgIcon = DeactivatedOrg; @@ -207,20 +201,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 | null = null; + protected canAccessPremium: boolean | null = null; + protected allCollections: CollectionView[] | null = null; protected allOrganizations: Organization[] = []; - protected ciphers: C[]; - protected collections: CollectionView[]; - protected isEmpty: boolean; + protected ciphers: C[] | null = null; + protected collections: CollectionView[] | null = null; + 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 BehaviorSubject(undefined); private destroy$ = new Subject(); private vaultItemDialogRef?: DialogRef | undefined; @@ -230,7 +224,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$, @@ -728,7 +722,7 @@ export class VaultComponent implements OnInit, OnDestr async handleUnknownCipher() { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("unknownCipher"), }); await this.router.navigate([], { @@ -858,9 +852,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 => { @@ -927,7 +925,10 @@ export class VaultComponent implements OnInit, OnDestr organizationId: cipher.organizationId as OrganizationId, }); - const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed); + const result = await lastValueFrom(dialogRef.closed); + if (result === undefined) { + return; + } if ( result.action === AttachmentDialogResult.Uploaded || @@ -981,7 +982,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 @@ -1103,6 +1104,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 @@ -1127,6 +1131,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) { @@ -1178,7 +1185,7 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("deletedCollectionId", collection.name), }); if (navigateAway) { @@ -1211,13 +1218,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, - ); + availableCollections = + this.allCollections?.filter((c) => c.organizationId === organization?.id) ?? []; } let ciphersToAssign: CipherView[]; @@ -1270,19 +1276,19 @@ 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; + return false; } if (!c.edit) { this.showMissingPermissionsError(); - return; + return false; } if (!(await this.repromptCipher([c]))) { - return; + return false; } if (CipherViewLikeUtils.isArchived(c)) { @@ -1296,13 +1302,14 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: toastMessage, }); this.refresh(); } catch (e) { this.logService.error(e); } + return true; }; async bulkRestore(ciphers: C[]) { @@ -1326,7 +1333,7 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("nothingSelected"), }); return; @@ -1336,23 +1343,25 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreManyWithServer(selectedCipherIds, activeUserId); this.toastService.showToast({ variant: "success", - title: null, + title: "", 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)))), ); @@ -1360,7 +1369,7 @@ export class VaultComponent implements OnInit, OnDestr } } - async deleteCipher(c: C): Promise { + async deleteCipher(c: C) { if (!(await this.repromptCipher([c]))) { return; } @@ -1379,7 +1388,7 @@ export class VaultComponent implements OnInit, OnDestr }); if (!confirmed) { - return false; + return; } try { @@ -1388,7 +1397,7 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"), }); this.refresh(); @@ -1405,7 +1414,7 @@ export class VaultComponent implements OnInit, OnDestr if (ciphers.length === 0 && collections.length === 0) { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("nothingSelected"), }); return; @@ -1445,7 +1454,7 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("nothingSelected"), }); return; @@ -1469,11 +1478,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") { @@ -1486,15 +1492,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; } @@ -1509,10 +1515,14 @@ 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, + title: "", message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), }); @@ -1529,6 +1539,14 @@ export class VaultComponent implements OnInit, OnDestr } } + showErrorToast() { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("unexpectedError"), + }); + } + /** * Toggles the favorite status of the cipher and updates it on the server. */ @@ -1540,7 +1558,7 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t( cipherFullView.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites", ), @@ -1555,15 +1573,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) { @@ -1588,7 +1606,7 @@ export class VaultComponent implements OnInit, OnDestr private showMissingPermissionsError() { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("missingPermissions"), }); }