diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts index 8c637d22b17..5fdac63e932 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts @@ -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({ t: (key) => key }), @@ -60,7 +63,9 @@ describe("VaultBannersComponent", () => { useValue: mock(), }, ], - }).compileComponents(); + }) + .overrideProvider(VaultBannersService, { useValue: bannerService }) + .compileComponents(); }); beforeEach(() => { diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts index e612bc231da..5a53f57ad19 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts @@ -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[] = []; diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 9e69286277d..7803b1c32f2 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -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 { diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts index 6c451344c9f..490c07d7538 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts @@ -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, diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 106f5b0d70d..a7331c73151 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -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", }) 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 1de2dd3b5bc..71c025f276a 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -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; - // 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 => { @@ -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", diff --git a/apps/web/src/app/vault/individual-vault/vault.module.ts b/apps/web/src/app/vault/individual-vault/vault.module.ts index c79c64c1ebf..a6cb41bacb8 100644 --- a/apps/web/src/app/vault/individual-vault/vault.module.ts +++ b/apps/web/src/app/vault/individual-vault/vault.module.ts @@ -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 {} diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 081d1e503e8..31764fcf058 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -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; diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 07d65656d2f..a89fc14f0db 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -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", diff --git a/apps/web/src/app/vault/org-vault/vault.module.ts b/apps/web/src/app/vault/org-vault/vault.module.ts index a478307123c..7a874ad612c 100644 --- a/apps/web/src/app/vault/org-vault/vault.module.ts +++ b/apps/web/src/app/vault/org-vault/vault.module.ts @@ -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 {}