1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-29 15:53:45 +00:00

fix bulk share in vault

This commit is contained in:
jaasen-livefront
2026-01-27 09:56:06 -08:00
parent 36b648f5d7
commit 1d6f0bd7db

View File

@@ -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<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;
kdfIterations: number;
trashCleanupWarning: string | null = null;
kdfIterations: number | null = null;
activeFilter: VaultFilter = new VaultFilter();
protected deactivatedOrgIcon = DeactivatedOrg;
@@ -207,20 +201,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 | 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<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 BehaviorSubject(undefined);
private destroy$ = new Subject<void>();
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
@@ -230,7 +224,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$,
@@ -728,7 +722,7 @@ export class VaultComponent<C extends CipherViewLike> 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<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 => {
@@ -927,7 +925,10 @@ export class VaultComponent<C extends CipherViewLike> 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<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
@@ -1103,6 +1104,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
@@ -1127,6 +1131,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) {
@@ -1178,7 +1185,7 @@ export class VaultComponent<C extends CipherViewLike> 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<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,
);
availableCollections =
this.allCollections?.filter((c) => c.organizationId === organization?.id) ?? [];
}
let ciphersToAssign: CipherView[];
@@ -1270,19 +1276,19 @@ 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;
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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<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)))),
);
@@ -1360,7 +1369,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;
}
@@ -1379,7 +1388,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
});
if (!confirmed) {
return false;
return;
}
try {
@@ -1388,7 +1397,7 @@ export class VaultComponent<C extends CipherViewLike> 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<C extends CipherViewLike> 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<C extends CipherViewLike> 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<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") {
@@ -1486,15 +1492,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;
}
@@ -1509,10 +1515,14 @@ 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,
title: "",
message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
});
@@ -1529,6 +1539,14 @@ export class VaultComponent<C extends CipherViewLike> 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<C extends CipherViewLike> 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<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) {
@@ -1588,7 +1606,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
private showMissingPermissionsError() {
this.toastService.showToast({
variant: "error",
title: null,
title: "",
message: this.i18nService.t("missingPermissions"),
});
}