From 52fa9d2bdd6ae595064e86e8eaf2b16c690e98f4 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Thu, 16 Jul 2026 10:00:40 -0700 Subject: [PATCH] [PM-40510] feat(vault): rename vault-filter query param to sharedFolderId Behind the vfo1-foundation flag, RoutedVaultFilterService now reads both sharedFolderId and legacy collectionId query params (preferring the new one) and, in createRoute(), writes sharedFolderId while nulling collectionId when the flag is on (reverse when off). Split from PM-40252 (terminology-only). --- .../routed-vault-filter.service.spec.ts | 95 +++++++++++++++++++ .../services/routed-vault-filter.service.ts | 14 ++- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 libs/vault/src/services/routed-vault-filter.service.spec.ts diff --git a/libs/vault/src/services/routed-vault-filter.service.spec.ts b/libs/vault/src/services/routed-vault-filter.service.spec.ts new file mode 100644 index 000000000000..b5a09c0aeacd --- /dev/null +++ b/libs/vault/src/services/routed-vault-filter.service.spec.ts @@ -0,0 +1,95 @@ +import { TestBed } from "@angular/core/testing"; +import { ActivatedRoute, convertToParamMap, ParamMap } from "@angular/router"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; + +import { CollectionId } from "@bitwarden/common/types/guid"; + +import { RoutedVaultFilterModel } from "../models/routed-vault-filter.model"; + +import { RoutedVaultFilterService } from "./routed-vault-filter.service"; +import { Vfo1TerminologyService } from "./vfo1-terminology.service"; + +describe("RoutedVaultFilterService", () => { + function setup(flagEnabled: boolean, queryParams: Record = {}) { + const paramMap = new BehaviorSubject(convertToParamMap({})); + const queryParamMap = new BehaviorSubject(convertToParamMap(queryParams)); + + TestBed.configureTestingModule({ + providers: [ + RoutedVaultFilterService, + { provide: ActivatedRoute, useValue: { paramMap, queryParamMap } }, + { provide: Vfo1TerminologyService, useValue: { enabled: () => flagEnabled } }, + ], + }); + + return TestBed.inject(RoutedVaultFilterService); + } + + describe("filter$ reader", () => { + it("reads the collection from the legacy collectionId query param", async () => { + const service = setup(false, { collectionId: "col-legacy" }); + + const filter = await firstValueFrom(service.filter$); + + expect(filter.collectionId).toBe("col-legacy"); + }); + + it("reads the collection from the new sharedFolderId query param", async () => { + const service = setup(true, { sharedFolderId: "col-new" }); + + const filter = await firstValueFrom(service.filter$); + + expect(filter.collectionId).toBe("col-new"); + }); + + it("prefers sharedFolderId over collectionId when both are present", async () => { + const service = setup(true, { sharedFolderId: "col-new", collectionId: "col-legacy" }); + + const filter = await firstValueFrom(service.filter$); + + expect(filter.collectionId).toBe("col-new"); + }); + + it("leaves the collection undefined when neither param is present", async () => { + const service = setup(false, {}); + + const filter = await firstValueFrom(service.filter$); + + expect(filter.collectionId).toBeUndefined(); + }); + }); + + describe("createRoute writer", () => { + const filter: RoutedVaultFilterModel = { collectionId: "col-1" as CollectionId }; + + it("writes collectionId and clears sharedFolderId when the flag is off", () => { + const service = setup(false); + + const [, extras] = service.createRoute(filter); + + expect(extras?.queryParams).toMatchObject({ + collectionId: "col-1", + sharedFolderId: null, + }); + }); + + it("writes sharedFolderId and clears collectionId when the flag is on", () => { + const service = setup(true); + + const [, extras] = service.createRoute(filter); + + expect(extras?.queryParams).toMatchObject({ + collectionId: null, + sharedFolderId: "col-1", + }); + }); + + it("merges query params so unrelated params are preserved", () => { + const service = setup(true); + + const [, extras] = service.createRoute(filter); + + expect(extras?.queryParamsHandling).toBe("merge"); + }); + }); +}); diff --git a/libs/vault/src/services/routed-vault-filter.service.ts b/libs/vault/src/services/routed-vault-filter.service.ts index e0d9f765361b..784fc0c0974b 100644 --- a/libs/vault/src/services/routed-vault-filter.service.ts +++ b/libs/vault/src/services/routed-vault-filter.service.ts @@ -10,6 +10,8 @@ import { RoutedVaultFilterModel, } from "../models/routed-vault-filter.model"; +import { Vfo1TerminologyService } from "./vfo1-terminology.service"; + /** * Injection token for the base route path used in vault filter navigation. */ @@ -26,6 +28,7 @@ export const VAULT_FILTER_BASE_ROUTE = new SafeInjectionToken("VaultFilt export class RoutedVaultFilterService implements OnDestroy { private onDestroy = new Subject(); private baseRoute: string = inject(VAULT_FILTER_BASE_ROUTE, { optional: true }) ?? ""; + private terminology = inject(Vfo1TerminologyService); /** * Filter values extracted from the URL. @@ -40,7 +43,10 @@ export class RoutedVaultFilterService implements OnDestroy { const type = isRoutedVaultFilterItemType(unsafeType) ? unsafeType : undefined; return { - collectionId: (queryParams.get("collectionId") as CollectionId) ?? undefined, + // Accept both param names during the shared-folder terminology transition. + collectionId: + ((queryParams.get("sharedFolderId") ?? + queryParams.get("collectionId")) as CollectionId | null) ?? undefined, folderId: queryParams.get("folderId") ?? undefined, organizationId: (params.get("organizationId") as OrganizationId) ?? @@ -72,9 +78,13 @@ export class RoutedVaultFilterService implements OnDestroy { */ createRoute(filter: RoutedVaultFilterModel): [commands: any[], extras?: NavigationExtras] { const commands: string[] = this.baseRoute ? [this.baseRoute] : []; + // When the flag is on, write the new `sharedFolderId` param and clear the legacy + // `collectionId` (queryParamsHandling: "merge" removes any param set to null). + const useSharedFolderParam = this.terminology.enabled(); const extras: NavigationExtras = { queryParams: { - collectionId: filter.collectionId ?? null, + collectionId: useSharedFolderParam ? null : (filter.collectionId ?? null), + sharedFolderId: useSharedFolderParam ? (filter.collectionId ?? null) : null, folderId: filter.folderId ?? null, organizationId: filter.organizationIdParamType === "path" ? null : (filter.organizationId ?? null),