From 1c4deaab9433008f0488cc0f2f29742e73476253 Mon Sep 17 00:00:00 2001 From: Leslie Xiong Date: Wed, 10 Dec 2025 19:45:36 -0500 Subject: [PATCH] migrated vault filters using router params --- apps/desktop/src/app/app-routing.module.ts | 20 ++ .../app/layout/desktop-layout.component.html | 2 +- .../app/layout/desktop-layout.component.ts | 4 +- .../src/services/vault-state.service.ts | 108 ---------- .../app/vault-v3/nav/vault-nav.component.html | 9 - .../app/vault-v3/nav/vault-nav.component.ts | 15 -- .../filters/collection-filter.component.html | 107 +++------- .../filters/collection-filter.component.ts | 36 +++- .../filters/folder-filter.component.html | 117 +++-------- .../filters/folder-filter.component.ts | 35 +++- .../organization-filter.component.html | 178 ++++------------- .../filters/organization-filter.component.ts | 103 +++++++--- .../filters/status-filter.component.html | 93 +++------ .../filters/status-filter.component.spec.ts | 98 --------- .../filters/status-filter.component.ts | 55 ++++-- .../filters/type-filter.component.html | 49 +---- .../filters/type-filter.component.ts | 38 ++-- .../vault-filter/shared-filter-imports.ts | 9 + .../vault-filter/vault-filter.component.html | 81 +++----- .../vault-filter/vault-filter.component.ts | 105 +++++++++- .../vault-filter/vault-filter.module.ts | 34 ---- .../src/vault/app/vault-v3/vault.component.ts | 187 ++++++++---------- 22 files changed, 573 insertions(+), 910 deletions(-) delete mode 100644 apps/desktop/src/services/vault-state.service.ts delete mode 100644 apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.html delete mode 100644 apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.ts delete mode 100644 apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.spec.ts create mode 100644 apps/desktop/src/vault/app/vault-v3/vault-filter/shared-filter-imports.ts delete mode 100644 apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.module.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 8fab7df1cd8..f822a9ae4d9 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -41,11 +41,19 @@ import { NewDeviceVerificationComponent, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui"; +import { + VaultFilterServiceAbstraction, + VaultFilterService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, +} from "@bitwarden/vault"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; +import { DesktopPremiumUpgradePromptService } from "../services/desktop-premium-upgrade-prompt.service"; import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault-v3/vault.component"; @@ -337,6 +345,18 @@ const routes: Routes = [ path: "", component: DesktopLayoutComponent, canActivate: [authGuard], + providers: [ + RoutedVaultFilterService, + RoutedVaultFilterBridgeService, + { + provide: VaultFilterServiceAbstraction, + useClass: VaultFilterService, + }, + { + provide: PremiumUpgradePromptService, + useClass: DesktopPremiumUpgradePromptService, + }, + ], children: [ { path: "new-vault", diff --git a/apps/desktop/src/app/layout/desktop-layout.component.html b/apps/desktop/src/app/layout/desktop-layout.component.html index c7dca09427a..dd4c77f1021 100644 --- a/apps/desktop/src/app/layout/desktop-layout.component.html +++ b/apps/desktop/src/app/layout/desktop-layout.component.html @@ -2,7 +2,7 @@ - + diff --git a/apps/desktop/src/app/layout/desktop-layout.component.ts b/apps/desktop/src/app/layout/desktop-layout.component.ts index b3f708cbbe8..798b8a629a3 100644 --- a/apps/desktop/src/app/layout/desktop-layout.component.ts +++ b/apps/desktop/src/app/layout/desktop-layout.component.ts @@ -5,7 +5,7 @@ import { PasswordManagerLogo } from "@bitwarden/assets/svg"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { VaultNavComponent } from "../../vault/app/vault-v3/nav/vault-nav.component"; +import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component"; import { DesktopSideNavComponent } from "./desktop-side-nav.component"; @@ -19,7 +19,7 @@ import { DesktopSideNavComponent } from "./desktop-side-nav.component"; LayoutComponent, NavigationModule, DesktopSideNavComponent, - VaultNavComponent, + VaultFilterComponent, ], templateUrl: "./desktop-layout.component.html", }) diff --git a/apps/desktop/src/services/vault-state.service.ts b/apps/desktop/src/services/vault-state.service.ts deleted file mode 100644 index e262ba2dc0e..00000000000 --- a/apps/desktop/src/services/vault-state.service.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; - -import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { SearchBarService } from "../app/layout/search/search-bar.service"; - -/** - * Service to coordinate vault state, including filter state and folder actions, - * between the navigation component and the vault component. - */ -@Injectable({ providedIn: "root" }) -export class VaultStateService { - private filterChangeSubject = new Subject(); - private addFolderSubject = new Subject(); - private editFolderSubject = new Subject(); - - /** - * The currently active vault filter. - */ - activeFilter: VaultFilter = new VaultFilter(); - - /** - * Observable stream of vault filter changes. - * Subscribe to this to react to filter changes from the navigation. - */ - readonly filterChange$ = this.filterChangeSubject.asObservable(); - - /** - * Observable stream of add folder requests. - * Subscribe to this to handle folder creation. - */ - readonly addFolder$ = this.addFolderSubject.asObservable(); - - /** - * Observable stream of edit folder requests. - * Subscribe to this to handle folder editing. - * Emits the folder ID to edit. - */ - readonly editFolder$ = this.editFolderSubject.asObservable(); - - constructor( - private i18nService: I18nService, - private searchBarService: SearchBarService, - ) {} - - /** - * Apply a new vault filter. - * This updates the search bar placeholder and notifies all subscribers. - */ - applyFilter(filter: VaultFilter): void { - // Store the active filter - this.activeFilter = filter; - - // Update search bar placeholder text based on the filter - this.searchBarService.setPlaceholderText( - this.i18nService.t(this.calculateSearchBarLocalizationString(filter)), - ); - - // Emit the filter change to subscribers - this.filterChangeSubject.next(filter); - } - - /** - * Request to add a new folder. - * This will notify subscribers to show the folder creation dialog. - */ - requestAddFolder(): void { - this.addFolderSubject.next(); - } - - /** - * Request to edit an existing folder. - * This will notify subscribers to show the folder edit dialog. - */ - requestEditFolder(folderId: string): void { - this.editFolderSubject.next(folderId); - } - - /** - * Calculate the appropriate search bar localization string based on the active filter. - */ - private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { - if (vaultFilter.status === "favorites") { - return "searchFavorites"; - } - if (vaultFilter.status === "trash") { - return "searchTrash"; - } - if (vaultFilter.cipherType != null) { - return "searchType"; - } - if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") { - return "searchFolder"; - } - if (vaultFilter.selectedCollectionId != null) { - return "searchCollection"; - } - if (vaultFilter.selectedOrganizationId != null) { - return "searchOrganization"; - } - if (vaultFilter.myVaultOnly) { - return "searchMyVault"; - } - return "searchVault"; - } -} diff --git a/apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.html b/apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.html deleted file mode 100644 index 3c8e5eb16da..00000000000 --- a/apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.ts b/apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.ts deleted file mode 100644 index d30be43ecb8..00000000000 --- a/apps/desktop/src/vault/app/vault-v3/nav/vault-nav.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ChangeDetectionStrategy, Component } from "@angular/core"; -import { NavigationModule } from "@bitwarden/components"; -import { I18nPipe } from "@bitwarden/ui-common"; -import { VaultStateService } from "../../../../services/vault-state.service"; -import { VaultFilterModule } from "../vault-filter/vault-filter.module"; - -@Component({ - selector: "app-vault-nav", - imports: [I18nPipe, NavigationModule, VaultFilterModule], - templateUrl: "./vault-nav.component.html", - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class VaultNavComponent { - constructor(protected vaultStateService: VaultStateService) {} -} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.html index f83a2e1c91f..735188f479c 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.html @@ -1,84 +1,23 @@ - -
-

- -

-
-
    - -
  • - - - - -
      - - -
    -
  • -
    - - -
-
+@if (collection().children.length) { + + @for (childCollection of collection().children; track childCollection.node.id) { + + } + +} @else { + +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts index 015b301efdb..6a98b8c2f09 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts @@ -1,12 +1,40 @@ -import { Component } from "@angular/core"; +import { Component, input, computed } from "@angular/core"; -import { CollectionFilterComponent as BaseCollectionFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/collection-filter.component"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { VaultFilter, CollectionFilter } from "@bitwarden/vault"; + +import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-collection-filter", templateUrl: "collection-filter.component.html", - standalone: false, + standalone: true, + imports: [...VAULT_FILTER_IMPORTS], }) -export class CollectionFilterComponent extends BaseCollectionFilterComponent {} +export class CollectionFilterComponent { + protected readonly collection = input>(); + protected readonly activeFilter = input(); + + protected readonly displayName = computed(() => { + return this.collection().node.name; + }); + + protected readonly isActive = computed(() => { + return ( + this.collection().node.id === this.activeFilter()?.collectionId && + !!this.activeFilter()?.selectedCollectionNode + ); + }); + + protected applyFilter(event: Event) { + event.stopPropagation(); + + const filter = this.activeFilter(); + + if (filter) { + filter.selectedCollectionNode = this.collection(); + } + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.html index a2240b03ff5..b201913d258 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.html @@ -1,94 +1,23 @@ - -
-

- -

- -
-
    - -
  • - - - - - -
      - - -
    -
  • -
    - -
-
+@if (folder().children.length) { + + @for (childFolder of folder().children; track childFolder.node.id) { + + } + +} @else { + +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts index f340e4082b8..477325c69a3 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts @@ -1,12 +1,39 @@ -import { Component } from "@angular/core"; +import { Component, input, computed } from "@angular/core"; -import { FolderFilterComponent as BaseFolderFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/folder-filter.component"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { VaultFilter, FolderFilter } from "@bitwarden/vault"; + +import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-folder-filter", templateUrl: "folder-filter.component.html", - standalone: false, + standalone: true, + imports: [...VAULT_FILTER_IMPORTS], }) -export class FolderFilterComponent extends BaseFolderFilterComponent {} +export class FolderFilterComponent { + protected readonly folder = input>(); + protected readonly activeFilter = input(); + + protected readonly displayName = computed(() => { + return this.folder().node.name; + }); + + protected readonly isActive = computed(() => { + return ( + this.folder().node.id === this.activeFilter()?.folderId && + !!this.activeFilter()?.selectedFolderNode + ); + }); + + protected applyFilter(event: Event) { + event.stopPropagation(); + const filter = this.activeFilter(); + + if (filter) { + filter.selectedFolderNode = this.folder(); + } + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.html index 8c73891dc09..60fa048406a 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.html @@ -1,140 +1,38 @@ - - - -
- -   -

- -

-
-
    -
  • - - - - - - -
  • -
-
- -
- -   -

- -

-
-
    -
  • - - - -
  • -
  • - - - - - - -
  • -
-
-
-
-
+@if (show()) { + @for (organization of organizations().children ?? []; track organization.node.id) { + @if (getOrgCollections(organization.node.id)?.children?.length > 0) { + + @if (!hideCollections() && collections() != null) { + @for (c of getOrgCollections(organization.node.id)?.children ?? []; track c.node.id) { + + } + } + + } @else { + + } + @if (!organization.node.enabled) { + + + + } + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts index 99338ddbb7c..2c92c35a2b5 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts @@ -1,53 +1,102 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component } from "@angular/core"; +import { Component, computed, input, inject } from "@angular/core"; -import { OrganizationFilterComponent as BaseOrganizationFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/organization-filter.component"; import { DisplayMode } from "@bitwarden/angular/vault/vault-filter/models/display-mode"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { ToastService } from "@bitwarden/components"; +import { OrganizationFilter, VaultFilter, CollectionFilter } from "@bitwarden/vault"; + +import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports"; + +import { CollectionFilterComponent } from "./collection-filter.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-filter", templateUrl: "organization-filter.component.html", - standalone: false, + standalone: true, + imports: [...VAULT_FILTER_IMPORTS, CollectionFilterComponent], }) -export class OrganizationFilterComponent extends BaseOrganizationFilterComponent { - get show() { +export class OrganizationFilterComponent { + private toastService: ToastService = inject(ToastService); + private i18nService: I18nService = inject(I18nService); + + protected readonly hide = input(false); + protected readonly organizations = input>(); + protected readonly activeFilter = input(); + protected readonly activeOrganizationDataOwnership = input(false); + protected readonly activeSingleOrganizationPolicy = input(false); + protected readonly hideCollections = input(false); + protected readonly collections = input>(); + + protected readonly show = computed(() => { const hiddenDisplayModes: DisplayMode[] = [ "singleOrganizationAndOrganizatonDataOwnershipPolicies", ]; return ( - !this.hide && - this.organizations.length > 0 && - hiddenDisplayModes.indexOf(this.displayMode) === -1 + !this.hide() && + this.organizations()?.children.length > 0 && + hiddenDisplayModes.indexOf(this.displayMode()) === -1 ); - } + }); - constructor( - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private toastService: ToastService, - ) { - super(); - } + protected readonly displayMode = computed(() => { + let displayMode: DisplayMode = "organizationMember"; + if (this.organizations() == null || this.organizations().children.length < 1) { + displayMode = "noOrganizations"; + } else if (this.activeOrganizationDataOwnership() && !this.activeSingleOrganizationPolicy()) { + displayMode = "organizationDataOwnershipPolicy"; + } else if (!this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) { + displayMode = "singleOrganizationPolicy"; + } else if (this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) { + displayMode = "singleOrganizationAndOrganizatonDataOwnershipPolicies"; + } - async applyOrganizationFilter(organization: Organization) { - if (organization.enabled) { - //proceed with default behaviour for enabled organizations - // 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 - super.applyOrganizationFilter(organization); - } else { + return displayMode; + }); + + protected applyFilter(organization: TreeNode) { + if (!organization.node.enabled) { this.toastService.showToast({ variant: "error", title: null, message: this.i18nService.t("disabledOrganizationFilterError"), }); + return; + } + + const filter = this.activeFilter(); + + if (filter) { + filter.selectedOrganizationNode = organization; } } + + private readonly collectionsByOrganization = computed(() => { + const collections = this.collections(); + const map = new Map>(); + const orgs = this.organizations()?.children; + + if (!collections || !orgs) { + return map; + } + + for (const org of orgs) { + const filteredCollections = collections.children.filter( + (node) => node.node.organizationId === org.node.id, + ); + + const headNode = new TreeNode(collections.node, null); + headNode.children = filteredCollections; + map.set(org.node.id, headNode); + } + + return map; + }); + + protected getOrgCollections(organizationId: OrganizationId): TreeNode { + return this.collectionsByOrganization().get(organizationId) ?? null; + } } diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html index 8b064778444..2f747c7a821 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html @@ -1,68 +1,25 @@ - -

{{ "filters" | i18n }}

-
    -
  • - - - -
  • -
  • - - - -
  • -
  • - - - - @if (!(canArchive$ | async)) { - - } -
  • -
  • - - - -
  • -
-
+@if (show()) { + @if (!hideArchive()) { + + @if (!(canArchive$ | async)) { + + } + } + @if (!hideTrash()) { + + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.spec.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.spec.ts deleted file mode 100644 index ba785310a0a..00000000000 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; -import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - -import { StatusFilterComponent } from "./status-filter.component"; - -describe("StatusFilterComponent", () => { - let component: StatusFilterComponent; - let fixture: ComponentFixture; - let cipherArchiveService: jest.Mocked; - let accountService: FakeAccountService; - - const mockUserId = Utils.newGuid() as UserId; - const event = new Event("click"); - - beforeEach(async () => { - accountService = mockAccountServiceWith(mockUserId); - cipherArchiveService = mock(); - - await TestBed.configureTestingModule({ - declarations: [StatusFilterComponent], - providers: [ - { provide: AccountService, useValue: accountService }, - { provide: CipherArchiveService, useValue: cipherArchiveService }, - { provide: PremiumUpgradePromptService, useValue: mock() }, - { - provide: BillingAccountProfileStateService, - useValue: mock(), - }, - { provide: I18nService, useValue: { t: (key: string) => key } }, - ], - imports: [JslibModule, PremiumBadgeComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(StatusFilterComponent); - component = fixture.componentInstance; - component.activeFilter = new VaultFilter(); - fixture.detectChanges(); - }); - - describe("handleArchiveFilter", () => { - const applyFilter = jest.fn(); - let promptForPremiumSpy: jest.SpyInstance; - - beforeEach(() => { - applyFilter.mockClear(); - component["applyFilter"] = applyFilter; - - promptForPremiumSpy = jest.spyOn(component["premiumBadgeComponent"]()!, "promptForPremium"); - }); - - it("should apply archive filter when userCanArchive returns true", async () => { - cipherArchiveService.userCanArchive$.mockReturnValue(of(true)); - cipherArchiveService.archivedCiphers$.mockReturnValue(of([])); - - await component["handleArchiveFilter"](event); - - expect(applyFilter).toHaveBeenCalledWith("archive"); - expect(promptForPremiumSpy).not.toHaveBeenCalled(); - }); - - it("should apply archive filter when userCanArchive returns false but hasArchivedCiphers is true", async () => { - const mockCipher = new CipherView(); - mockCipher.id = "test-id"; - - cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); - cipherArchiveService.archivedCiphers$.mockReturnValue(of([mockCipher])); - - await component["handleArchiveFilter"](event); - - expect(applyFilter).toHaveBeenCalledWith("archive"); - expect(promptForPremiumSpy).not.toHaveBeenCalled(); - }); - - it("should prompt for premium when userCanArchive returns false and hasArchivedCiphers is false", async () => { - cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); - cipherArchiveService.archivedCiphers$.mockReturnValue(of([])); - - await component["handleArchiveFilter"](event); - - expect(applyFilter).not.toHaveBeenCalled(); - expect(promptForPremiumSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.ts index 95ffd3f0212..22f52a69dc5 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.ts @@ -1,20 +1,60 @@ -import { Component, viewChild } from "@angular/core"; +import { Component, viewChild, input, inject, computed } from "@angular/core"; import { combineLatest, firstValueFrom, map, switchMap } from "rxjs"; import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; -import { StatusFilterComponent as BaseStatusFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/status-filter.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { VaultFilter, CipherStatus, CipherTypeFilter } from "@bitwarden/vault"; + +import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-status-filter", templateUrl: "status-filter.component.html", - standalone: false, + standalone: true, + imports: [...VAULT_FILTER_IMPORTS, PremiumBadgeComponent], }) -export class StatusFilterComponent extends BaseStatusFilterComponent { +export class StatusFilterComponent { + private accountService: AccountService = inject(AccountService); + private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService); + + protected readonly hideTrash = input(false); + protected readonly hideArchive = input(false); + protected readonly activeFilter = input(); + protected readonly archiveFilter: CipherTypeFilter = { + id: "archive", + name: "archiveNoun", + type: "archive", + icon: "bwi-archive", + }; + protected readonly trashFilter: CipherTypeFilter = { + id: "trash", + name: "trash", + type: "trash", + icon: "bwi-trash", + }; + + protected readonly show = computed(() => { + return !(this.hideTrash() && this.hideArchive()); + }); + + protected applyFilter(filterType: CipherStatus) { + let filter: CipherTypeFilter = null; + if (filterType === "archive") { + filter = this.archiveFilter; + } else if (filterType === "trash") { + filter = this.trashFilter; + } + + if (filter) { + this.activeFilter().selectedCipherTypeNode = new TreeNode(filter, null); + } + } + private readonly premiumBadgeComponent = viewChild(PremiumBadgeComponent); private userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -28,13 +68,6 @@ export class StatusFilterComponent extends BaseStatusFilterComponent { ), ); - constructor( - private accountService: AccountService, - private cipherArchiveService: CipherArchiveService, - ) { - super(); - } - protected async handleArchiveFilter(event: Event) { const [canArchive, hasArchivedCiphers] = await firstValueFrom( combineLatest([this.canArchive$, this.hasArchivedCiphers$]), diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.html index f8a83e01266..fdaf74be74f 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.html @@ -1,39 +1,10 @@ -
-

- -

-
-
    - @for (typeFilter of typeFilters$ | async; track typeFilter) { -
  • - - - -
  • - } -
+@for (typeFilter of typeFilters$ | async; track typeFilter) { + +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts index fbab7ce4667..56c272ccfd7 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts @@ -1,34 +1,48 @@ -import { Component } from "@angular/core"; +import { Component, input, inject } from "@angular/core"; import { map, shareReplay } from "rxjs"; -import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; -import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; +import { VaultFilter, CipherTypeFilter } from "@bitwarden/vault"; + +import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-type-filter", templateUrl: "type-filter.component.html", - standalone: false, + standalone: true, + imports: [...VAULT_FILTER_IMPORTS], }) -export class TypeFilterComponent extends BaseTypeFilterComponent { +export class TypeFilterComponent { + private restrictedItemTypesService: RestrictedItemTypesService = inject( + RestrictedItemTypesService, + ); + + protected readonly cipherTypes = input>(); + protected readonly activeFilter = input(); + + protected applyFilter(cipherType: TreeNode) { + const filter = this.activeFilter(); + + if (filter) { + filter.selectedCipherTypeNode = cipherType; + } + } + protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe( map((restrictedItemTypes) => // Filter out restricted item types from the typeFilters array - CIPHER_MENU_ITEMS.filter( - (typeFilter) => + this.cipherTypes().children.filter( + (type) => !restrictedItemTypes.some( (restrictedType) => restrictedType.allowViewOrgIds.length === 0 && - restrictedType.cipherType === typeFilter.type, + restrictedType.cipherType === type.node.type, ), ), ), shareReplay({ bufferSize: 1, refCount: true }), ); - - constructor(private restrictedItemTypesService: RestrictedItemTypesService) { - super(); - } } diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/shared-filter-imports.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/shared-filter-imports.ts new file mode 100644 index 00000000000..d2cd3d9cf66 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/shared-filter-imports.ts @@ -0,0 +1,9 @@ +import { CommonModule } from "@angular/common"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { NavigationModule } from "@bitwarden/components"; + +/** + * Common imports shared across all vault filter components. + */ +export const VAULT_FILTER_IMPORTS = [CommonModule, JslibModule, NavigationModule] as const; diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.html index 14e72f3bb9d..521deabae3b 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.html @@ -1,51 +1,30 @@ -
- -
- - - - - - - +@if (!isLoaded) { +
+ +
+} @else { + + + + + @if (!hideFolders()) { + + @for (folder of (folders$ | async)?.children ?? []; track folder.node.id) { + + } + + } + +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts index d7c5bafc3a4..709e9afd190 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts @@ -1,12 +1,109 @@ -import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, inject, OnInit, input, output } from "@angular/core"; +import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; -import { VaultFilterComponent as BaseVaultFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/vault-filter.component"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { NavigationModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { + OrganizationFilter, + CipherTypeFilter, + CollectionFilter, + FolderFilter, + VaultFilter, + VaultFilterServiceAbstraction as VaultFilterService, + RoutedVaultFilterBridgeService, +} from "@bitwarden/vault"; + +import { FolderFilterComponent } from "./filters/folder-filter.component"; +import { OrganizationFilterComponent } from "./filters/organization-filter.component"; +import { StatusFilterComponent } from "./filters/status-filter.component"; +import { TypeFilterComponent } from "./filters/type-filter.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-filter", templateUrl: "vault-filter.component.html", - standalone: false, + standalone: true, + imports: [ + I18nPipe, + NavigationModule, + CommonModule, + OrganizationFilterComponent, + StatusFilterComponent, + TypeFilterComponent, + FolderFilterComponent, + ], }) -export class VaultFilterComponent extends BaseVaultFilterComponent {} +export class VaultFilterComponent implements OnInit { + private routedVaultFilterBridgeService = inject(RoutedVaultFilterBridgeService); + private vaultFilterService: VaultFilterService = inject(VaultFilterService); + private accountService: AccountService = inject(AccountService); + private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService); + private policyService: PolicyService = inject(PolicyService); + private componentIsDestroyed$ = new Subject(); + + protected activeFilter: VaultFilter; + protected readonly hideFolders = input(false); + protected readonly hideCollections = input(false); + protected readonly hideFavorites = input(false); + protected readonly hideTrash = input(false); + protected readonly hideOrganizations = input(false); + protected onFilterChange = output(); + + private activeUserId: UserId; + protected isLoaded = false; + protected showArchiveVaultFilter = false; + protected activeOrganizationDataOwnershipPolicy: boolean; + protected activeSingleOrganizationPolicy: boolean; + protected organizations$: Observable>; + protected collections$: Observable>; + protected folders$: Observable>; + protected cipherTypes$: Observable>; + + private async setActivePolicies() { + this.activeOrganizationDataOwnershipPolicy = await firstValueFrom( + this.policyService.policyAppliesToUser$( + PolicyType.OrganizationDataOwnership, + this.activeUserId, + ), + ); + this.activeSingleOrganizationPolicy = await firstValueFrom( + this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, this.activeUserId), + ); + } + + async ngOnInit(): Promise { + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.organizations$ = this.vaultFilterService.organizationTree$; + if ( + this.organizations$ != null && + (await firstValueFrom(this.organizations$)).children.length > 0 + ) { + await this.setActivePolicies(); + } + this.cipherTypes$ = this.vaultFilterService.cipherTypeTree$; + this.folders$ = this.vaultFilterService.folderTree$; + this.collections$ = this.vaultFilterService.collectionTree$; + + this.showArchiveVaultFilter = await firstValueFrom( + this.cipherArchiveService.hasArchiveFlagEnabled$(), + ); + + // Subscribe to the active filter from the bridge service + this.routedVaultFilterBridgeService.activeFilter$ + .pipe(takeUntil(this.componentIsDestroyed$)) + .subscribe((filter) => { + this.activeFilter = filter; + }); + + this.isLoaded = true; + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.module.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.module.ts deleted file mode 100644 index 54a6d33ca6a..00000000000 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; - -import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/vault/abstractions/deprecated-vault-filter.service"; -import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service"; - -import { CollectionFilterComponent } from "./filters/collection-filter.component"; -import { FolderFilterComponent } from "./filters/folder-filter.component"; -import { OrganizationFilterComponent } from "./filters/organization-filter.component"; -import { StatusFilterComponent } from "./filters/status-filter.component"; -import { TypeFilterComponent } from "./filters/type-filter.component"; -import { VaultFilterComponent } from "./vault-filter.component"; - -@NgModule({ - imports: [CommonModule, JslibModule, PremiumBadgeComponent], - declarations: [ - VaultFilterComponent, - CollectionFilterComponent, - FolderFilterComponent, - OrganizationFilterComponent, - StatusFilterComponent, - TypeFilterComponent, - ], - exports: [VaultFilterComponent], - providers: [ - { - provide: DeprecatedVaultFilterServiceAbstraction, - useClass: VaultFilterService, - }, - ], -}) -export class VaultFilterModule {} diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.ts index 6cb5eb05379..cbc8c5c866d 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts @@ -9,15 +9,21 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observable } from "rxjs"; +import { + firstValueFrom, + Subject, + takeUntil, + switchMap, + lastValueFrom, + Observable, + from, +} from "rxjs"; import { filter, map, take } from "rxjs/operators"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; -import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -28,7 +34,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -60,8 +65,6 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { - AddEditFolderDialogComponent, - AddEditFolderDialogResult, AttachmentDialogResult, AttachmentsV2Component, ChangeLoginPasswordService, @@ -78,17 +81,17 @@ import { PasswordRepromptService, CipherFormComponent, ArchiveCipherUtilitiesService, + VaultFilter, + RoutedVaultFilterBridgeService, + VaultFilterServiceAbstraction as VaultFilterService, } from "@bitwarden/vault"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service"; import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; -import { VaultStateService } from "../../../services/vault-state.service"; import { invokeMenu, RendererMenuItem } from "../../../utils"; import { AssignCollectionsDesktopComponent } from "../vault/assign-collections"; import { ItemFooterComponent } from "../vault/item-footer.component"; -import { VaultFilterComponent } from "../vault/vault-filter/vault-filter.component"; -import { VaultFilterModule } from "../vault/vault-filter/vault-filter.module"; import { VaultItemsV2Component } from "../vault/vault-items-v2.component"; const BroadcasterSubscriptionId = "VaultComponent"; @@ -108,7 +111,6 @@ const BroadcasterSubscriptionId = "VaultComponent"; ItemModule, ButtonModule, PremiumBadgeComponent, - VaultFilterModule, VaultItemsV2Component, ], providers: [ @@ -135,17 +137,11 @@ const BroadcasterSubscriptionId = "VaultComponent"; }, ], }) -export class VaultComponent - implements OnInit, OnDestroy, CopyClickListener -{ +export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(VaultItemsV2Component, { static: true }) - vaultItemsComponent: VaultItemsV2Component | null = null; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild(VaultFilterComponent, { static: true }) - vaultFilterComponent: VaultFilterComponent | null = null; + vaultItemsComponent: VaultItemsV2Component | null = null; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) @@ -195,6 +191,7 @@ export class VaultComponent private componentIsDestroyed$ = new Subject(); private allOrganizations: Organization[] = []; private allCollections: CollectionView[] = []; + private filteredCollections: CollectionView[] = []; constructor( private route: ActivatedRoute, @@ -210,7 +207,6 @@ export class VaultComponent private totpService: TotpService, private passwordRepromptService: PasswordRepromptService, private searchBarService: SearchBarService, - private apiService: ApiService, private dialogService: DialogService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, @@ -221,12 +217,12 @@ export class VaultComponent private collectionService: CollectionService, private organizationService: OrganizationService, private folderService: FolderService, - private configService: ConfigService, private authRequestService: AuthRequestServiceAbstraction, private cipherArchiveService: CipherArchiveService, private policyService: PolicyService, private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService, - private vaultStateService: VaultStateService, + private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService, + private vaultFilterService: VaultFilterService, ) {} async ngOnInit() { @@ -242,26 +238,10 @@ export class VaultComponent this.userHasPremiumAccess = canAccessPremium; }); - // Subscribe to filter changes from VaultNavComponent - this.vaultStateService.filterChange$ + // Subscribe to filter changes from router params via the bridge service + this.routedVaultFilterBridgeService.activeFilter$ .pipe( - switchMap((vaultFilter: VaultFilter) => this.applyVaultFilter(vaultFilter)), - takeUntil(this.componentIsDestroyed$), - ) - .subscribe(); - - // Subscribe to add folder requests from VaultNavComponent - this.vaultStateService.addFolder$ - .pipe( - switchMap(() => this.addFolder()), - takeUntil(this.componentIsDestroyed$), - ) - .subscribe(); - - // Subscribe to edit folder requests from VaultNavComponent - this.vaultStateService.editFolder$ - .pipe( - switchMap((folderId: string) => this.editFolder(folderId)), + switchMap((vaultFilter: VaultFilter) => from(this.applyVaultFilter(vaultFilter))), takeUntil(this.componentIsDestroyed$), ) .subscribe(); @@ -293,15 +273,12 @@ export class VaultComponent break; case "syncCompleted": if (this.vaultItemsComponent) { - await this.vaultItemsComponent - .reload(this.activeFilter.buildFilter()) - .catch(() => {}); - } - if (this.vaultFilterComponent) { - await this.vaultFilterComponent - .reloadCollectionsAndFolders(this.activeFilter) - .catch(() => {}); - await this.vaultFilterComponent.reloadOrganizations().catch(() => {}); + // const filterFn = this.wrapFilterForCipherListView( + // this.activeFilter.buildFilter(), + // ); + // await this.vaultItemsComponent.reload(filterFn).catch(() => {}); + const filter = this.activeFilter.buildFilter(); + await this.vaultItemsComponent.reload(filter).catch(() => {}); } break; case "modalShown": @@ -403,6 +380,12 @@ export class VaultComponent .subscribe((collections) => { this.allCollections = collections; }); + + this.vaultFilterService.filteredCollections$ + .pipe(takeUntil(this.componentIsDestroyed$)) + .subscribe((collections) => { + this.filteredCollections = collections; + }); } ngOnDestroy() { @@ -429,19 +412,6 @@ export class VaultComponent this.addType = paramCipherAddType; await this.addCipher(this.addType).catch(() => {}); } - - const paramCipherType = toCipherType(params.type); - this.activeFilter = new VaultFilter({ - status: params.deleted ? "trash" : params.favorites ? "favorites" : "all", - cipherType: params.action === "add" || paramCipherType == null ? undefined : paramCipherType, - selectedFolderId: params.folderId, - selectedCollectionId: params.selectedCollectionId, - selectedOrganizationId: params.selectedOrganizationId, - myVaultOnly: params.myVaultOnly ?? false, - }); - if (this.vaultItemsComponent) { - await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {}); - } } /** @@ -465,9 +435,7 @@ export class VaultComponent this.cipherId = cipher.id; this.cipher = cipher; this.collections = - this.vaultFilterComponent?.collections?.fullList.filter((c) => - cipher.collectionIds.includes(c.id), - ) ?? null; + this.filteredCollections?.filter((c) => cipher.collectionIds.includes(c.id)) ?? null; this.action = "view"; await this.go().catch(() => {}); @@ -824,19 +792,45 @@ export class VaultComponent await this.go().catch(() => {}); } + /** + * Wraps a filter function to handle CipherListView objects. + * CipherListView has a different type structure where type can be a string or object. + * This wrapper converts it to CipherView-compatible structure before filtering. + */ + private wrapFilterForCipherListView( + filterFn: (cipher: CipherView) => boolean, + ): (cipher: CipherViewLike) => boolean { + return (cipher: CipherViewLike) => { + // For CipherListView, create a proxy object with the correct type property + if (CipherViewLikeUtils.isCipherListView(cipher)) { + const proxyCipher = { + ...cipher, + type: CipherViewLikeUtils.getType(cipher), + // Normalize undefined organizationId to null for filter compatibility + organizationId: cipher.organizationId ?? null, + // Explicitly include isDeleted and isArchived since they might be getters + isDeleted: CipherViewLikeUtils.isDeleted(cipher), + isArchived: CipherViewLikeUtils.isArchived(cipher), + }; + return filterFn(proxyCipher as any); + } + }; + } + async applyVaultFilter(vaultFilter: VaultFilter) { this.searchBarService.setPlaceholderText( this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)), ); this.activeFilter = vaultFilter; - await this.vaultItemsComponent - ?.reload( - this.activeFilter.buildFilter(), - vaultFilter.status === "trash", - vaultFilter.status === "archive", - ) - .catch(() => {}); - await this.go().catch(() => {}); + + const originalFilterFn = this.activeFilter.buildFilter(); + const wrappedFilterFn = this.wrapFilterForCipherListView(originalFilterFn); + + await this.vaultItemsComponent?.reload( + wrappedFilterFn, + vaultFilter.isDeleted, + vaultFilter.isArchived, + ); } private getAvailableCollections(cipher: CipherView): CollectionView[] { @@ -850,25 +844,25 @@ export class VaultComponent } private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { - if (vaultFilter.status === "favorites") { + if (vaultFilter.isFavorites) { return "searchFavorites"; } - if (vaultFilter.status === "trash") { + if (vaultFilter.isDeleted) { return "searchTrash"; } if (vaultFilter.cipherType != null) { return "searchType"; } - if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") { + if (vaultFilter.folderId != null && vaultFilter.folderId !== "none") { return "searchFolder"; } - if (vaultFilter.selectedCollectionId != null) { + if (vaultFilter.collectionId != null) { return "searchCollection"; } - if (vaultFilter.selectedOrganizationId != null) { + if (vaultFilter.organizationId != null) { return "searchOrganization"; } - if (vaultFilter.myVaultOnly) { + if (vaultFilter.isMyVaultSelected) { return "searchMyVault"; } return "searchVault"; @@ -889,23 +883,6 @@ export class VaultComponent if (!folderView) { return; } - - const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, { - editFolderConfig: { - folder: { - ...folderView, - }, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if ( - result === AddEditFolderDialogResult.Deleted || - result === AddEditFolderDialogResult.Created - ) { - await this.vaultFilterComponent?.reloadCollectionsAndFolders(this.activeFilter); - } } /** Refresh the current cipher object */ @@ -992,22 +969,22 @@ export class VaultComponent } private prefillCipherFromFilter() { - if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) { - const collections = this.vaultFilterComponent.collections?.fullList.filter( - (c) => c.id === this.activeFilter.selectedCollectionId, + if (this.activeFilter.collectionId != null) { + const collections = this.filteredCollections?.filter( + (c) => c.id === this.activeFilter.collectionId, ); - if (collections.length > 0) { + if (collections?.length > 0) { this.addOrganizationId = collections[0].organizationId; - this.addCollectionIds = [this.activeFilter.selectedCollectionId]; + this.addCollectionIds = [this.activeFilter.collectionId]; } - } else if (this.activeFilter.selectedOrganizationId) { - this.addOrganizationId = this.activeFilter.selectedOrganizationId; + } else if (this.activeFilter.organizationId) { + this.addOrganizationId = this.activeFilter.organizationId; } else { // clear out organizationId when the user switches to a personal vault filter this.addOrganizationId = null; } - if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { - this.folderId = this.activeFilter.selectedFolderId; + if (this.activeFilter.folderId && this.activeFilter.selectedFolderNode) { + this.folderId = this.activeFilter.folderId; } if (this.config == null) {