1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 02:19:18 +00:00

[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
This commit is contained in:
Jordan Aasen
2026-02-13 11:36:39 -08:00
committed by GitHub
parent 2912bf05e1
commit 323f30c8e9
3 changed files with 81 additions and 80 deletions

View File

@@ -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<boolean> => {
restore = async (c: CipherViewLike): Promise<void> => {
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;
}
};

View File

@@ -100,7 +100,7 @@ export interface VaultItemDialogParams {
/**
* Function to restore a cipher from the trash.
*/
restore?: (c: CipherViewLike) => Promise<boolean>;
restore?: (c: CipherViewLike) => Promise<void>;
}
export const VaultItemDialogResult = {

View File

@@ -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<EmptyStateType, EmptyStateItem>;
],
})
export class VaultComponent<C extends CipherViewLike> 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<C>;
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<C extends CipherViewLike> 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<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string> = this.route.queryParams.pipe(
map((queryParams) => queryParams.search),
);
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
private refresh$ = new Subject<void>();
private destroy$ = new Subject<void>();
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
@@ -220,7 +207,7 @@ export class VaultComponent<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<OrganizationFilter>;
await this.filterComponent.filters?.organizationFilter?.action(orgNode);
await this.filterComponent()?.filters?.organizationFilter?.action(orgNode);
}
addFolder = (): void => {
@@ -912,7 +902,10 @@ export class VaultComponent<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> implements OnInit, OnDestr
async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise<void> {
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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> implements OnInit, OnDestr
await this.editCipher(cipher, true);
}
restore = async (c: C): Promise<boolean> => {
restore = async (c: CipherViewLike) => {
let toastMessage;
if (!CipherViewLikeUtils.isDeleted(c)) {
return;
@@ -1281,13 +1279,14 @@ export class VaultComponent<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C>[]) {
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<C extends CipherViewLike> implements OnInit, OnDestr
}
}
async deleteCipher(c: C): Promise<boolean> {
async deleteCipher(c: C) {
if (!(await this.repromptCipher([c]))) {
return;
}
@@ -1364,7 +1363,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
});
if (!confirmed) {
return false;
return;
}
try {
@@ -1373,7 +1372,6 @@ export class VaultComponent<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> implements OnInit, OnDestr
private showMissingPermissionsError() {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("missingPermissions"),
});
}
@@ -1584,13 +1585,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
*/
private async getPasswordFromCipherViewLike(cipher: C): Promise<string | undefined> {
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;
}
}