1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-11 10:10:25 +01:00

[PM-6992] Refactor VaultComponent (#10126)

* Added function to return cipherview observable and trigger the decryption process if the cipherviews$ observable returns empty

* Updated the vault component to use getCipherViews$ observable function

* converted vault banner to standalone component

* converted vault header to standalone component

* fixed unawaited promises

converted component to standalone component

* cleaned up vault module

* fixed imports

* refactored getCipherView$ observable

* refactored onVaultItemsEvent to switch case

* Refactored to use toast service instead of platform utils service for toast

* Added function to return cipherview observable and trigger the decryption process if the cipherviews$ observable returns empty

* Updated the vault component to use getCipherViews$ observable function

* converted vault banner to standalone component

* converted vault header to standalone component

* fixed unawaited promises

converted component to standalone component

* cleaned up vault module

* fixed imports

* refactored getCipherView$ observable

* refactored onVaultItemsEvent to switch case

* Refactored to use toast service instead of platform utils service for toast

* merged with main and fixed conflicts

* reordered standalone property

* converted components to standalone

* cleaned up ng module for org vault

* cleaned up vault module individual vault

* fixed conflicts

* Replaced deprecated toast service

* refactored to use switch case for org vault

* fixed comments and fixed failing tests

reverted to use getAllDecrypted
This commit is contained in:
SmithThe4th 2024-07-23 16:47:54 -04:00 committed by GitHub
parent b32f6182a5
commit 91aa475766
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 336 additions and 224 deletions

View File

@ -1,5 +1,6 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { RouterTestingModule } from "@angular/router/testing";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
@ -11,7 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { BannerComponent, BannerModule } from "@bitwarden/components";
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
import { LooseComponentsModule } from "../../../shared";
import { SharedModule } from "../../../shared";
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
import { VaultBannersComponent } from "./vault-banners.component";
@ -36,13 +37,15 @@ describe("VaultBannersComponent", () => {
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
await TestBed.configureTestingModule({
imports: [BannerModule, LooseComponentsModule, VerifyEmailComponent],
declarations: [VaultBannersComponent, I18nPipe],
imports: [
BannerModule,
SharedModule,
VerifyEmailComponent,
VaultBannersComponent,
RouterTestingModule,
],
declarations: [I18nPipe],
providers: [
{
provide: VaultBannersService,
useValue: bannerService,
},
{
provide: I18nService,
useValue: mock<I18nService>({ t: (key) => key }),
@ -60,7 +63,9 @@ describe("VaultBannersComponent", () => {
useValue: mock<TokenService>(),
},
],
}).compileComponents();
})
.overrideProvider(VaultBannersService, { useValue: bannerService })
.compileComponents();
});
beforeEach(() => {

View File

@ -1,11 +1,19 @@
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { BannerModule } from "@bitwarden/components";
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
import { SharedModule } from "../../../shared";
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
@Component({
standalone: true,
selector: "app-vault-banners",
templateUrl: "./vault-banners.component.html",
imports: [VerifyEmailComponent, SharedModule, BannerModule],
providers: [VaultBannersService],
})
export class VaultBannersComponent implements OnInit {
visibleBanners: VisibleVaultBanner[] = [];

View File

@ -1,3 +1,4 @@
import { CommonModule } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
@ -8,14 +9,19 @@ import {
} from "@angular/core";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { BreadcrumbsModule, MenuModule } from "@bitwarden/components";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { CollectionDialogTabType } from "../../components/collection-dialog";
import { PipesModule } from "../pipes/pipes.module";
import {
All,
RoutedVaultFilterModel,
@ -23,8 +29,18 @@ import {
} from "../vault-filter/shared/models/routed-vault-filter.model";
@Component({
standalone: true,
selector: "app-vault-header",
templateUrl: "./vault-header.component.html",
imports: [
CommonModule,
MenuModule,
SharedModule,
BreadcrumbsModule,
HeaderModule,
PipesModule,
JslibModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultHeaderComponent implements OnInit {

View File

@ -158,10 +158,9 @@ describe("VaultOnboardingComponent", () => {
});
it("should set installExtension to true when hasBWInstalled command is passed", async () => {
const saveCompletedTasksSpy = jest.spyOn(
(component as any).vaultOnboardingService,
"setVaultOnboardingTasks",
);
const saveCompletedTasksSpy = jest
.spyOn((component as any).vaultOnboardingService, "setVaultOnboardingTasks")
.mockReturnValue(Promise.resolve());
(component as any).vaultOnboardingService.vaultOnboardingState$ = of({
createAccount: true,

View File

@ -24,11 +24,17 @@ import { LinkModule } from "@bitwarden/components";
import { OnboardingModule } from "../../../shared/components/onboarding/onboarding.module";
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
import { VaultOnboardingTasks } from "./services/vault-onboarding.service";
import { VaultOnboardingService, VaultOnboardingTasks } from "./services/vault-onboarding.service";
@Component({
standalone: true,
imports: [OnboardingModule, CommonModule, JslibModule, LinkModule],
providers: [
{
provide: VaultOnboardingServiceAbstraction,
useClass: VaultOnboardingService,
},
],
selector: "app-vault-onboarding",
templateUrl: "vault-onboarding.component.html",
})

View File

@ -60,6 +60,7 @@ import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { DialogService, Icons, ToastService } from "@bitwarden/components";
import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault";
import { SharedModule } from "../../shared/shared.module";
import { AssignCollectionsWebComponent } from "../components/assign-collections";
import {
CollectionDialogAction,
@ -68,6 +69,7 @@ import {
} from "../components/collection-dialog";
import { VaultItem } from "../components/vault-items/vault-item";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { getNestedCollectionTree } from "../utils/collection-utils";
import { AddEditComponent } from "./add-edit.component";
@ -87,6 +89,7 @@ import {
import { openIndividualVaultCollectionsDialog } from "./collections.component";
import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component";
import { ShareComponent } from "./share.component";
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
import { RoutedVaultFilterBridgeService } from "./vault-filter/services/routed-vault-filter-bridge.service";
@ -99,13 +102,25 @@ import {
} from "./vault-filter/shared/models/routed-vault-filter.model";
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
const BroadcasterSubscriptionId = "VaultComponent";
const SearchTextDebounceInterval = 200;
@Component({
standalone: true,
selector: "app-vault",
templateUrl: "vault.component.html",
imports: [
VaultHeaderComponent,
VaultOnboardingComponent,
VaultBannersComponent,
VaultFilterModule,
VaultItemsModule,
SharedModule,
],
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
})
export class VaultComponent implements OnInit, OnDestroy {
@ -323,18 +338,14 @@ export class VaultComponent implements OnInit, OnDestroy {
const cipherId = getCipherIdFromParams(params);
if (cipherId) {
if ((await this.cipherService.get(cipherId)) != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.editCipherId(cipherId);
await this.editCipherId(cipherId);
} else {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("unknownCipher"),
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([], {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("unknownCipher"),
});
await this.router.navigate([], {
queryParams: { itemId: null, cipherId: null },
queryParamsHandling: "merge",
});
@ -403,36 +414,48 @@ export class VaultComponent implements OnInit, OnDestroy {
async onVaultItemsEvent(event: VaultItemEvent) {
this.processingEvent = true;
try {
if (event.type === "viewAttachments") {
await this.editCipherAttachments(event.item);
} else if (event.type === "viewCipherCollections") {
await this.editCipherCollections(event.item);
} else if (event.type === "clone") {
await this.cloneCipher(event.item);
} else if (event.type === "restore") {
if (event.items.length === 1) {
await this.restore(event.items[0]);
} else {
await this.bulkRestore(event.items);
}
} else if (event.type === "delete") {
await this.handleDeleteEvent(event.items);
} else if (event.type === "moveToFolder") {
await this.bulkMove(event.items);
} else if (event.type === "moveToOrganization") {
if (event.items.length === 1) {
await this.shareCipher(event.items[0]);
} else {
await this.bulkShare(event.items);
}
} else if (event.type === "copyField") {
await this.copy(event.item, event.field);
} else if (event.type === "editCollection") {
await this.editCollection(event.item, CollectionDialogTabType.Info);
} else if (event.type === "viewCollectionAccess") {
await this.editCollection(event.item, CollectionDialogTabType.Access);
} else if (event.type === "assignToCollections") {
await this.bulkAssignToCollections(event.items);
switch (event.type) {
case "viewAttachments":
await this.editCipherAttachments(event.item);
break;
case "viewCipherCollections":
await this.editCipherCollections(event.item);
break;
case "clone":
await this.cloneCipher(event.item);
break;
case "restore":
if (event.items.length === 1) {
await this.restore(event.items[0]);
} else {
await this.bulkRestore(event.items);
}
break;
case "delete":
await this.handleDeleteEvent(event.items);
break;
case "moveToFolder":
await this.bulkMove(event.items);
break;
case "moveToOrganization":
if (event.items.length === 1) {
await this.shareCipher(event.items[0]);
} else {
await this.bulkShare(event.items);
}
break;
case "copyField":
await this.copy(event.item, event.field);
break;
case "editCollection":
await this.editCollection(event.item, CollectionDialogTabType.Info);
break;
case "viewCollectionAccess":
await this.editCollection(event.item, CollectionDialogTabType.Access);
break;
case "assignToCollections":
await this.bulkAssignToCollections(event.items);
break;
}
} finally {
this.processingEvent = false;
@ -445,9 +468,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$);
const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode<OrganizationFilter>;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.filterComponent.filters?.organizationFilter?.action(orgNode);
await this.filterComponent.filters?.organizationFilter?.action(orgNode);
}
addFolder = async (): Promise<void> => {
@ -670,7 +691,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.refresh();
// Navigate away if we deleted the collection we were viewing
if (this.selectedCollection?.node.id === c?.id) {
void this.router.navigate([], {
await this.router.navigate([], {
queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null },
queryParamsHandling: "merge",
replaceUrl: true,
@ -697,14 +718,15 @@ export class VaultComponent implements OnInit, OnDestroy {
try {
await this.apiService.deleteCollection(collection.organizationId, collection.id);
await this.collectionService.delete(collection.id);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("deletedCollectionId", collection.name),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedCollectionId", collection.name),
});
// Navigate away if we deleted the collection we were viewing
if (this.selectedCollection?.node.id === collection.id) {
void this.router.navigate([], {
await this.router.navigate([], {
queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null },
queryParamsHandling: "merge",
replaceUrl: true,
@ -722,11 +744,11 @@ export class VaultComponent implements OnInit, OnDestroy {
}
if (ciphers.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("nothingSelected"),
});
return;
}
@ -790,7 +812,11 @@ export class VaultComponent implements OnInit, OnDestroy {
try {
await this.cipherService.restoreWithServer(c.id);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItem"),
});
this.refresh();
} catch (e) {
this.logService.error(e);
@ -809,12 +835,20 @@ export class VaultComponent implements OnInit, OnDestroy {
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
if (selectedCipherIds.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("nothingSelected"),
});
return;
}
await this.cipherService.restoreManyWithServer(selectedCipherIds);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItems"),
});
this.refresh();
}
@ -862,11 +896,12 @@ export class VaultComponent implements OnInit, OnDestroy {
try {
await this.deleteCipherWithServer(c.id, permanent);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"),
});
this.refresh();
} catch (e) {
this.logService.error(e);
@ -883,7 +918,11 @@ export class VaultComponent implements OnInit, OnDestroy {
}
if (ciphers.length === 0 && collections.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("nothingSelected"),
});
return;
}
@ -926,7 +965,11 @@ export class VaultComponent implements OnInit, OnDestroy {
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
if (selectedCipherIds.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("nothingSelected"),
});
return;
}
@ -958,7 +1001,11 @@ export class VaultComponent implements OnInit, OnDestroy {
value = await this.totpService.getCode(cipher.login.totp);
typeI18nKey = "verificationCodeTotp";
} else {
this.platformUtilsService.showToast("info", null, this.i18nService.t("unexpectedError"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("unexpectedError"),
});
return;
}
@ -974,20 +1021,19 @@ export class VaultComponent implements OnInit, OnDestroy {
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
);
this.toastService.showToast({
variant: "info",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
});
if (field === "password") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
await this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
} else if (field === "totp") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, cipher.id);
await this.eventCollectionService.collect(
EventType.Cipher_ClientCopiedHiddenField,
cipher.id,
);
}
}
@ -1006,7 +1052,11 @@ export class VaultComponent implements OnInit, OnDestroy {
}
if (ciphers.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("nothingSelected"),
});
return;
}
@ -1056,9 +1106,7 @@ export class VaultComponent implements OnInit, OnDestroy {
};
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([], {
void this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: "merge",

View File

@ -1,30 +1,18 @@
import { NgModule } from "@angular/core";
import { BannerModule, BreadcrumbsModule } from "@bitwarden/components";
import { VerifyEmailComponent } from "../../auth/settings/verify-email.component";
import { LooseComponentsModule, SharedModule } from "../../shared";
import { CollectionDialogModule } from "../components/collection-dialog";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { CollectionBadgeModule } from "../org-vault/collection-badge/collection-badge.module";
import { GroupBadgeModule } from "../org-vault/group-badge/group-badge.module";
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
import { PipesModule } from "./pipes/pipes.module";
import { VaultBannersService } from "./vault-banners/services/vault-banners.service";
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
import { VaultOnboardingService } from "./vault-onboarding/services/vault-onboarding.service";
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
import { VaultRoutingModule } from "./vault-routing.module";
import { VaultComponent } from "./vault.component";
@NgModule({
imports: [
VaultFilterModule,
VaultRoutingModule,
OrganizationBadgeModule,
GroupBadgeModule,
@ -33,21 +21,8 @@ import { VaultComponent } from "./vault.component";
SharedModule,
LooseComponentsModule,
BulkDialogsModule,
BreadcrumbsModule,
VaultItemsModule,
CollectionDialogModule,
VaultOnboardingComponent,
BannerModule,
VerifyEmailComponent,
],
declarations: [VaultComponent, VaultHeaderComponent, VaultBannersComponent],
exports: [VaultComponent],
providers: [
VaultBannersService,
{
provide: VaultOnboardingServiceAbstraction,
useClass: VaultOnboardingService,
},
VaultComponent,
],
})
export class VaultModule {}

View File

@ -1,7 +1,9 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
@ -9,8 +11,16 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
import {
DialogService,
SimpleDialogOptions,
BreadcrumbsModule,
MenuModule,
SearchModule,
} from "@bitwarden/components";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
import { CollectionDialogTabType } from "../../components/collection-dialog";
import { CollectionAdminService } from "../../core/collection-admin.service";
@ -21,8 +31,18 @@ import {
} from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
@Component({
standalone: true,
selector: "app-org-vault-header",
templateUrl: "./vault-header.component.html",
imports: [
CommonModule,
MenuModule,
SharedModule,
BreadcrumbsModule,
HeaderModule,
SearchModule,
JslibModule,
],
})
export class VaultHeaderComponent implements OnInit {
protected All = All;

View File

@ -58,11 +58,12 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { DialogService, Icons, ToastService } from "@bitwarden/components";
import { DialogService, Icons, NoItemsModule, ToastService } from "@bitwarden/components";
import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault";
import { GroupService, GroupView } from "../../admin-console/organizations/core";
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component";
import { SharedModule } from "../../shared";
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
import { AssignCollectionsWebComponent } from "../components/assign-collections";
@ -72,6 +73,7 @@ import {
openCollectionDialog,
} from "../components/collection-dialog";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { CollectionAdminService } from "../core/collection-admin.service";
import { CollectionAdminView } from "../core/views/collection-admin.view";
import {
@ -87,6 +89,7 @@ import {
RoutedVaultFilterModel,
Unassigned,
} from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component";
import { getNestedCollectionTree } from "../utils/collection-utils";
import { AddEditComponent } from "./add-edit.component";
@ -95,7 +98,9 @@ import {
BulkCollectionsDialogComponent,
BulkCollectionsDialogResult,
} from "./bulk-collections-dialog";
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
import { openOrgVaultCollectionsDialog } from "./collections.component";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
const BroadcasterSubscriptionId = "OrgVaultComponent";
const SearchTextDebounceInterval = 200;
@ -106,8 +111,17 @@ enum AddAccessStatusType {
}
@Component({
standalone: true,
selector: "app-org-vault",
templateUrl: "vault.component.html",
imports: [
VaultHeaderComponent,
CollectionAccessRestrictedComponent,
VaultFilterModule,
VaultItemsModule,
SharedModule,
NoItemsModule,
],
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
})
export class VaultComponent implements OnInit, OnDestroy {
@ -577,7 +591,11 @@ export class VaultComponent implements OnInit, OnDestroy {
if (canEditCipher) {
await this.editCipherId(cipherId);
} else {
this.platformUtilsService.showToast("error", null, this.i18nService.t("unknownCipher"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("unknownCipher"),
});
await this.router.navigate([], {
queryParams: { cipherId: null, itemId: null },
queryParamsHandling: "merge",
@ -598,14 +616,14 @@ export class VaultComponent implements OnInit, OnDestroy {
}
const cipher = allCiphers$.find((c) => c.id === cipherId);
if (organization.useEvents && cipher != undefined) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.viewEvents(cipher);
await this.viewEvents(cipher);
} else {
this.platformUtilsService.showToast("error", null, this.i18nService.t("unknownCipher"));
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([], {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("unknownCipher"),
});
await this.router.navigate([], {
queryParams: { viewEvents: null },
queryParamsHandling: "merge",
});
@ -686,50 +704,65 @@ export class VaultComponent implements OnInit, OnDestroy {
this.processingEvent = true;
try {
if (event.type === "viewAttachments") {
await this.editCipherAttachments(event.item);
} else if (event.type === "viewCipherCollections") {
await this.editCipherCollections(event.item);
} else if (event.type === "clone") {
await this.cloneCipher(event.item);
} else if (event.type === "restore") {
if (event.items.length === 1) {
await this.restore(event.items[0]);
} else {
await this.bulkRestore(event.items);
switch (event.type) {
case "viewAttachments":
await this.editCipherAttachments(event.item);
break;
case "viewCipherCollections":
await this.editCipherCollections(event.item);
break;
case "clone":
await this.cloneCipher(event.item);
break;
case "restore":
if (event.items.length === 1) {
await this.restore(event.items[0]);
} else {
await this.bulkRestore(event.items);
}
break;
case "delete": {
const ciphers = event.items
.filter((i) => i.collection === undefined)
.map((i) => i.cipher);
const collections = event.items
.filter((i) => i.cipher === undefined)
.map((i) => i.collection);
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] as CollectionAdminView);
} else {
await this.bulkDelete(ciphers, collections, this.organization);
}
break;
}
} else if (event.type === "delete") {
const ciphers = event.items.filter((i) => i.collection === undefined).map((i) => i.cipher);
const collections = event.items
.filter((i) => i.cipher === undefined)
.map((i) => i.collection);
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] as CollectionAdminView);
} else {
await this.bulkDelete(ciphers, collections, this.organization);
}
} else if (event.type === "copyField") {
await this.copy(event.item, event.field);
} else if (event.type === "editCollection") {
await this.editCollection(
event.item as CollectionAdminView,
CollectionDialogTabType.Info,
event.readonly,
);
} else if (event.type === "viewCollectionAccess") {
await this.editCollection(
event.item as CollectionAdminView,
CollectionDialogTabType.Access,
event.readonly,
);
} else if (event.type === "bulkEditCollectionAccess") {
await this.bulkEditCollectionAccess(event.items, this.organization);
} else if (event.type === "assignToCollections") {
await this.bulkAssignToCollections(event.items);
} else if (event.type === "viewEvents") {
await this.viewEvents(event.item);
case "copyField":
await this.copy(event.item, event.field);
break;
case "editCollection":
await this.editCollection(
event.item as CollectionAdminView,
CollectionDialogTabType.Info,
event.readonly,
);
break;
case "viewCollectionAccess":
await this.editCollection(
event.item as CollectionAdminView,
CollectionDialogTabType.Access,
event.readonly,
);
break;
case "bulkEditCollectionAccess":
await this.bulkEditCollectionAccess(event.items, this.organization);
break;
case "assignToCollections":
await this.bulkAssignToCollections(event.items);
break;
case "viewEvents":
await this.viewEvents(event.item);
break;
}
} finally {
this.processingEvent = false;
@ -962,7 +995,11 @@ export class VaultComponent implements OnInit, OnDestroy {
this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled) ||
c.isUnassigned;
await this.cipherService.restoreWithServer(c.id, asAdmin);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItem"),
});
this.refresh();
} catch (e) {
this.logService.error(e);
@ -1008,11 +1045,11 @@ export class VaultComponent implements OnInit, OnDestroy {
}
if (unassignedCiphers.length === 0 && editAccessCiphers.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("nothingSelected"),
});
return;
}
@ -1023,7 +1060,11 @@ export class VaultComponent implements OnInit, OnDestroy {
);
}
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItems"),
});
this.refresh();
}
@ -1058,11 +1099,11 @@ export class VaultComponent implements OnInit, OnDestroy {
try {
await this.deleteCipherWithServer(c.id, permanent, c.isUnassigned);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"),
});
this.refresh();
} catch (e) {
this.logService.error(e);
@ -1085,11 +1126,11 @@ export class VaultComponent implements OnInit, OnDestroy {
}
try {
await this.apiService.deleteCollection(this.organization?.id, collection.id);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("deletedCollectionId", collection.name),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedCollectionId", collection.name),
});
// Navigate away if we deleted the collection we were viewing
if (this.selectedCollection?.node.id === collection.id) {
@ -1128,7 +1169,11 @@ export class VaultComponent implements OnInit, OnDestroy {
});
if (ciphers.length === 0 && collections.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("nothingSelected"),
});
return;
}
@ -1182,7 +1227,11 @@ export class VaultComponent implements OnInit, OnDestroy {
value = await this.totpService.getCode(cipher.login.totp);
typeI18nKey = "verificationCodeTotp";
} else {
this.platformUtilsService.showToast("info", null, this.i18nService.t("unexpectedError"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("unexpectedError"),
});
return;
}
@ -1198,20 +1247,19 @@ export class VaultComponent implements OnInit, OnDestroy {
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
);
this.toastService.showToast({
variant: "info",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
});
if (field === "password") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
await this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
} else if (field === "totp") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, cipher.id);
await this.eventCollectionService.collect(
EventType.Cipher_ClientCopiedHiddenField,
cipher.id,
);
}
}
@ -1310,7 +1358,11 @@ export class VaultComponent implements OnInit, OnDestroy {
async bulkAssignToCollections(items: CipherView[]) {
if (items.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("nothingSelected"),
});
return;
}
@ -1381,9 +1433,7 @@ export class VaultComponent implements OnInit, OnDestroy {
};
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([], {
void this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: "merge",

View File

@ -1,40 +1,25 @@
import { NgModule } from "@angular/core";
import { BreadcrumbsModule, NoItemsModule, SearchModule } from "@bitwarden/components";
import { LooseComponentsModule } from "../../shared/loose-components.module";
import { SharedModule } from "../../shared/shared.module";
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { CollectionDialogModule } from "../components/collection-dialog";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
import { CollectionBadgeModule } from "./collection-badge/collection-badge.module";
import { GroupBadgeModule } from "./group-badge/group-badge.module";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
import { VaultRoutingModule } from "./vault-routing.module";
import { VaultComponent } from "./vault.component";
@NgModule({
imports: [
VaultRoutingModule,
VaultFilterModule,
SharedModule,
LooseComponentsModule,
GroupBadgeModule,
CollectionBadgeModule,
OrganizationBadgeModule,
PipesModule,
BreadcrumbsModule,
VaultItemsModule,
CollectionDialogModule,
CollectionAccessRestrictedComponent,
NoItemsModule,
SearchModule,
VaultComponent,
],
declarations: [VaultComponent, VaultHeaderComponent],
exports: [VaultComponent],
})
export class VaultModule {}