1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 14:53:33 +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$);
collapsedNodes.delete("AllCollections");
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes);
const userId = await firstValueFrom(this.activeUserId$);
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes, userId);
}
protected async addCollectionFilter(): Promise<VaultFilterSection> {

View File

@@ -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<CipherTypeFilter>): Promise<void> => {

View File

@@ -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<TreeNode<FolderFilter>>;
collectionTree$: Observable<TreeNode<CollectionFilter>>;
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>>;
getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
setCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
expandOrgFilter: () => Promise<void>;
getOrganizationFilter: () => Observable<Organization>;
setOrganizationFilter: (organization: Organization) => void;
buildTypeTree: (
abstract getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
abstract setCollapsedFilterNodes: (
collapsedFilterNodes: Set<string>,
userId: UserId,
) => Promise<void>;
abstract expandOrgFilter: (userId: UserId) => Promise<void>;
abstract getOrganizationFilter: () => Observable<Organization>;
abstract setOrganizationFilter: (organization: Organization) => void;
abstract buildTypeTree: (
head: CipherTypeFilter,
array: CipherTypeFilter[],
) => Observable<TreeNode<CipherTypeFilter>>;
// 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;
}

View File

@@ -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<string[]>;
let collapsedGroupingsState: FakeSingleUserState<string[]>;
beforeEach(() => {
organizationService = mock<OrganizationService>();
@@ -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 () => {

View File

@@ -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<TreeNode<OrganizationFilter>> = combineLatest([
this.memberOrganizations$,
this.activeUserId$.pipe(
@@ -103,11 +110,9 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree();
private collapsedGroupingsState: ActiveUserState<string[]> =
this.stateProvider.getActive(COLLAPSED_GROUPINGS);
readonly collapsedFilterNodes$: Observable<Set<string>> =
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
private collapsedGroupingsState(userId: UserId): SingleUserState<string[]> {
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<CollectionFilter>;
}
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
await this.collapsedGroupingsState.update(() => Array.from(collapsedFilterNodes));
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>, userId: UserId): Promise<void> {
await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes));
}
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$);
if (!collapsedFilterNodes.has("AllVaults")) {
return;
}
collapsedFilterNodes.delete("AllVaults");
await this.setCollapsedFilterNodes(collapsedFilterNodes);
await this.setCollapsedFilterNodes(collapsedFilterNodes, userId);
}
protected async buildOrganizationTree(

View File

@@ -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<void>();
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