1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 23:03:32 +00:00

[PM-12035] Vault filter updates to use SingleUserState (#13641)

* vault filter use SingleUserState

* fixing tests

* Changes so that userId is passed to service, instead of access in service

* passing activeUserId from the components to service

* Sugggested changes

* updating functions to be abstract on vault-filter.service

* updating all functions to be abstract on vault filter service
This commit is contained in:
cd-bitwarden
2025-03-27 10:33:16 -04:00
committed by GitHub
parent 1887b75c77
commit 7efbf95482
6 changed files with 49 additions and 33 deletions

View File

@@ -89,8 +89,8 @@ export class VaultFilterComponent
const collapsedNodes = await firstValueFrom(this.vaultFilterService.collapsedFilterNodes$); const collapsedNodes = await firstValueFrom(this.vaultFilterService.collapsedFilterNodes$);
collapsedNodes.delete("AllCollections"); collapsedNodes.delete("AllCollections");
const userId = await firstValueFrom(this.activeUserId$);
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes); await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes, userId);
} }
protected async addCollectionFilter(): Promise<VaultFilterSection> { protected async addCollectionFilter(): Promise<VaultFilterSection> {

View File

@@ -94,6 +94,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
} }
private trialFlowService = inject(TrialFlowService); private trialFlowService = inject(TrialFlowService);
protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
constructor( constructor(
protected vaultFilterService: VaultFilterService, protected vaultFilterService: VaultFilterService,
@@ -162,7 +163,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
filter.selectedOrganizationNode = orgNode; filter.selectedOrganizationNode = orgNode;
} }
this.vaultFilterService.setOrganizationFilter(orgNode.node); this.vaultFilterService.setOrganizationFilter(orgNode.node);
await this.vaultFilterService.expandOrgFilter(); const userId = await firstValueFrom(this.activeUserId$);
await this.vaultFilterService.expandOrgFilter(userId);
}; };
applyTypeFilter = async (filterNode: TreeNode<CipherTypeFilter>): Promise<void> => { applyTypeFilter = async (filterNode: TreeNode<CipherTypeFilter>): Promise<void> => {

View File

@@ -4,6 +4,7 @@ import { Observable } from "rxjs";
import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common"; import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@@ -22,16 +23,19 @@ export abstract class VaultFilterService {
folderTree$: Observable<TreeNode<FolderFilter>>; folderTree$: Observable<TreeNode<FolderFilter>>;
collectionTree$: Observable<TreeNode<CollectionFilter>>; collectionTree$: Observable<TreeNode<CollectionFilter>>;
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>>; cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>>;
getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>; abstract getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
setCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>; abstract setCollapsedFilterNodes: (
expandOrgFilter: () => Promise<void>; collapsedFilterNodes: Set<string>,
getOrganizationFilter: () => Observable<Organization>; userId: UserId,
setOrganizationFilter: (organization: Organization) => void; ) => Promise<void>;
buildTypeTree: ( abstract expandOrgFilter: (userId: UserId) => Promise<void>;
abstract getOrganizationFilter: () => Observable<Organization>;
abstract setOrganizationFilter: (organization: Organization) => void;
abstract buildTypeTree: (
head: CipherTypeFilter, head: CipherTypeFilter,
array: CipherTypeFilter[], array: CipherTypeFilter[],
) => Observable<TreeNode<CipherTypeFilter>>; ) => Observable<TreeNode<CipherTypeFilter>>;
// TODO: Remove this from org vault when collection admin service adopts state management // TODO: Remove this from org vault when collection admin service adopts state management
reloadCollections?: (collections: CollectionAdminView[]) => void; abstract reloadCollections?: (collections: CollectionAdminView[]) => void;
clearOrganizationFilter: () => void; abstract clearOrganizationFilter: () => void;
} }

View File

@@ -2,7 +2,7 @@ import {
FakeAccountService, FakeAccountService,
mockAccountServiceWith, mockAccountServiceWith,
} from "@bitwarden/common/../spec/fake-account-service"; } 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 { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, ReplaySubject } from "rxjs"; import { firstValueFrom, ReplaySubject } from "rxjs";
@@ -42,7 +42,7 @@ describe("vault filter service", () => {
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService; let accountService: FakeAccountService;
let collapsedGroupingsState: FakeActiveUserState<string[]>; let collapsedGroupingsState: FakeSingleUserState<string[]>;
beforeEach(() => { beforeEach(() => {
organizationService = mock<OrganizationService>(); organizationService = mock<OrganizationService>();
@@ -83,21 +83,21 @@ describe("vault filter service", () => {
collectionService, collectionService,
accountService, accountService,
); );
collapsedGroupingsState = stateProvider.activeUser.getFake(COLLAPSED_GROUPINGS); collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS);
}); });
describe("collapsed filter nodes", () => { describe("collapsed filter nodes", () => {
const nodes = new Set(["1", "2"]); const nodes = new Set(["1", "2"]);
it("should update the collapsedFilterNodes$", async () => { it("should update the collapsedFilterNodes$", async () => {
await vaultFilterService.setCollapsedFilterNodes(nodes); await vaultFilterService.setCollapsedFilterNodes(nodes, mockUserId);
const collapsedGroupingsState = stateProvider.activeUser.getFake(COLLAPSED_GROUPINGS); const collapsedGroupingsState = stateProvider.singleUser.getFake(
expect(await firstValueFrom(collapsedGroupingsState.state$)).toEqual(Array.from(nodes));
expect(collapsedGroupingsState.nextMock).toHaveBeenCalledWith([
mockUserId, 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 () => { it("loads from state on initialization", async () => {

View File

@@ -23,8 +23,10 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -47,12 +49,17 @@ const NestingDelimiter = "/";
@Injectable() @Injectable()
export class VaultFilterService implements VaultFilterServiceAbstraction { 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( memberOrganizations$ = this.activeUserId$.pipe(
switchMap((id) => this.organizationService.memberOrganizations$(id)), switchMap((id) => this.organizationService.memberOrganizations$(id)),
); );
collapsedFilterNodes$ = this.activeUserId$.pipe(
switchMap((id) => this.collapsedGroupingsState(id).state$),
map((state) => new Set(state)),
);
organizationTree$: Observable<TreeNode<OrganizationFilter>> = combineLatest([ organizationTree$: Observable<TreeNode<OrganizationFilter>> = combineLatest([
this.memberOrganizations$, this.memberOrganizations$,
this.activeUserId$.pipe( this.activeUserId$.pipe(
@@ -103,11 +110,9 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree(); cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree();
private collapsedGroupingsState: ActiveUserState<string[]> = private collapsedGroupingsState(userId: UserId): SingleUserState<string[]> {
this.stateProvider.getActive(COLLAPSED_GROUPINGS); return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS);
}
readonly collapsedFilterNodes$: Observable<Set<string>> =
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
constructor( constructor(
protected organizationService: OrganizationService, protected organizationService: OrganizationService,
@@ -125,8 +130,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionFilter>; return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionFilter>;
} }
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> { async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>, userId: UserId): Promise<void> {
await this.collapsedGroupingsState.update(() => Array.from(collapsedFilterNodes)); await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes));
} }
protected async getCollapsedFilterNodes(): Promise<Set<string>> { protected async getCollapsedFilterNodes(): Promise<Set<string>> {
@@ -149,13 +154,13 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
} }
} }
async expandOrgFilter() { async expandOrgFilter(userId: UserId) {
const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$); const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$);
if (!collapsedFilterNodes.has("AllVaults")) { if (!collapsedFilterNodes.has("AllVaults")) {
return; return;
} }
collapsedFilterNodes.delete("AllVaults"); collapsedFilterNodes.delete("AllVaults");
await this.setCollapsedFilterNodes(collapsedFilterNodes); await this.setCollapsedFilterNodes(collapsedFilterNodes, userId);
} }
protected async buildOrganizationTree( protected async buildOrganizationTree(

View File

@@ -1,10 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core"; 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 { map } from "rxjs/operators";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { VaultFilterService } from "../../services/abstractions/vault-filter.service"; 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 { export class VaultFilterSectionComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private activeUserId$ = getUserId(this.accountService.activeAccount$);
@Input() activeFilter: VaultFilter; @Input() activeFilter: VaultFilter;
@Input() section: VaultFilterSection; @Input() section: VaultFilterSection;
@@ -29,6 +32,7 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
constructor( constructor(
private vaultFilterService: VaultFilterService, private vaultFilterService: VaultFilterService,
private injector: Injector, private injector: Injector,
private accountService: AccountService,
) { ) {
this.vaultFilterService.collapsedFilterNodes$ this.vaultFilterService.collapsedFilterNodes$
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
@@ -126,7 +130,8 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
} else { } else {
this.collapsedFilterNodes.add(node.id); 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 // an injector is necessary to pass data into a dynamic component