diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 43d8f910d0f..384390d738e 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -89,8 +89,8 @@ export class VaultFilterComponent const collapsedNodes = await firstValueFrom(this.vaultFilterService.collapsedFilterNodes$); collapsedNodes.delete("AllCollections"); - - await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes); + const userId = await firstValueFrom(this.activeUserId$); + await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes, userId); } protected async addCollectionFilter(): Promise { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 0a168157705..3f1e7755c8f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -94,6 +94,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } private trialFlowService = inject(TrialFlowService); + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); constructor( protected vaultFilterService: VaultFilterService, @@ -162,7 +163,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { filter.selectedOrganizationNode = orgNode; } this.vaultFilterService.setOrganizationFilter(orgNode.node); - await this.vaultFilterService.expandOrgFilter(); + const userId = await firstValueFrom(this.activeUserId$); + await this.vaultFilterService.expandOrgFilter(userId); }; applyTypeFilter = async (filterNode: TreeNode): Promise => { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index b8494c8aa54..0e3ee69a2c6 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -4,6 +4,7 @@ import { Observable } from "rxjs"; import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { UserId } from "@bitwarden/common/types/guid"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -22,16 +23,19 @@ export abstract class VaultFilterService { folderTree$: Observable>; collectionTree$: Observable>; cipherTypeTree$: Observable>; - getCollectionNodeFromTree: (id: string) => Promise>; - setCollapsedFilterNodes: (collapsedFilterNodes: Set) => Promise; - expandOrgFilter: () => Promise; - getOrganizationFilter: () => Observable; - setOrganizationFilter: (organization: Organization) => void; - buildTypeTree: ( + abstract getCollectionNodeFromTree: (id: string) => Promise>; + abstract setCollapsedFilterNodes: ( + collapsedFilterNodes: Set, + userId: UserId, + ) => Promise; + abstract expandOrgFilter: (userId: UserId) => Promise; + abstract getOrganizationFilter: () => Observable; + abstract setOrganizationFilter: (organization: Organization) => void; + abstract buildTypeTree: ( head: CipherTypeFilter, array: CipherTypeFilter[], ) => Observable>; // TODO: Remove this from org vault when collection admin service adopts state management - reloadCollections?: (collections: CollectionAdminView[]) => void; - clearOrganizationFilter: () => void; + abstract reloadCollections?: (collections: CollectionAdminView[]) => void; + abstract clearOrganizationFilter: () => void; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index f56931fa987..559d0cc60c5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -2,7 +2,7 @@ import { FakeAccountService, mockAccountServiceWith, } from "@bitwarden/common/../spec/fake-account-service"; -import { FakeActiveUserState } from "@bitwarden/common/../spec/fake-state"; +import { FakeSingleUserState } from "@bitwarden/common/../spec/fake-state"; import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, ReplaySubject } from "rxjs"; @@ -42,7 +42,7 @@ describe("vault filter service", () => { const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; - let collapsedGroupingsState: FakeActiveUserState; + let collapsedGroupingsState: FakeSingleUserState; beforeEach(() => { organizationService = mock(); @@ -83,21 +83,21 @@ describe("vault filter service", () => { collectionService, accountService, ); - collapsedGroupingsState = stateProvider.activeUser.getFake(COLLAPSED_GROUPINGS); + collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS); }); describe("collapsed filter nodes", () => { const nodes = new Set(["1", "2"]); it("should update the collapsedFilterNodes$", async () => { - await vaultFilterService.setCollapsedFilterNodes(nodes); + await vaultFilterService.setCollapsedFilterNodes(nodes, mockUserId); - const collapsedGroupingsState = stateProvider.activeUser.getFake(COLLAPSED_GROUPINGS); - expect(await firstValueFrom(collapsedGroupingsState.state$)).toEqual(Array.from(nodes)); - expect(collapsedGroupingsState.nextMock).toHaveBeenCalledWith([ + const collapsedGroupingsState = stateProvider.singleUser.getFake( mockUserId, - Array.from(nodes), - ]); + COLLAPSED_GROUPINGS, + ); + expect(await firstValueFrom(collapsedGroupingsState.state$)).toEqual(Array.from(nodes)); + expect(collapsedGroupingsState.nextMock).toHaveBeenCalledWith(Array.from(nodes)); }); it("loads from state on initialization", async () => { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index f3e4441af9f..058c84517cb 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -23,8 +23,10 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; +import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -47,12 +49,17 @@ const NestingDelimiter = "/"; @Injectable() export class VaultFilterService implements VaultFilterServiceAbstraction { - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); memberOrganizations$ = this.activeUserId$.pipe( switchMap((id) => this.organizationService.memberOrganizations$(id)), ); + collapsedFilterNodes$ = this.activeUserId$.pipe( + switchMap((id) => this.collapsedGroupingsState(id).state$), + map((state) => new Set(state)), + ); + organizationTree$: Observable> = combineLatest([ this.memberOrganizations$, this.activeUserId$.pipe( @@ -103,11 +110,9 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { cipherTypeTree$: Observable> = this.buildCipherTypeTree(); - private collapsedGroupingsState: ActiveUserState = - this.stateProvider.getActive(COLLAPSED_GROUPINGS); - - readonly collapsedFilterNodes$: Observable> = - this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c))); + private collapsedGroupingsState(userId: UserId): SingleUserState { + return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS); + } constructor( protected organizationService: OrganizationService, @@ -125,8 +130,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode; } - async setCollapsedFilterNodes(collapsedFilterNodes: Set): Promise { - await this.collapsedGroupingsState.update(() => Array.from(collapsedFilterNodes)); + async setCollapsedFilterNodes(collapsedFilterNodes: Set, userId: UserId): Promise { + await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes)); } protected async getCollapsedFilterNodes(): Promise> { @@ -149,13 +154,13 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { } } - async expandOrgFilter() { + async expandOrgFilter(userId: UserId) { const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$); if (!collapsedFilterNodes.has("AllVaults")) { return; } collapsedFilterNodes.delete("AllVaults"); - await this.setCollapsedFilterNodes(collapsedFilterNodes); + await this.setCollapsedFilterNodes(collapsedFilterNodes, userId); } protected async buildOrganizationTree( diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts index b231200a7cb..41329319805 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts @@ -1,10 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core"; -import { Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { map } from "rxjs/operators"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { VaultFilterService } from "../../services/abstractions/vault-filter.service"; @@ -17,6 +19,7 @@ import { VaultFilter } from "../models/vault-filter.model"; }) export class VaultFilterSectionComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); + private activeUserId$ = getUserId(this.accountService.activeAccount$); @Input() activeFilter: VaultFilter; @Input() section: VaultFilterSection; @@ -29,6 +32,7 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy { constructor( private vaultFilterService: VaultFilterService, private injector: Injector, + private accountService: AccountService, ) { this.vaultFilterService.collapsedFilterNodes$ .pipe(takeUntil(this.destroy$)) @@ -126,7 +130,8 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy { } else { this.collapsedFilterNodes.add(node.id); } - await this.vaultFilterService.setCollapsedFilterNodes(this.collapsedFilterNodes); + const userId = await firstValueFrom(this.activeUserId$); + await this.vaultFilterService.setCollapsedFilterNodes(this.collapsedFilterNodes, userId); } // an injector is necessary to pass data into a dynamic component