1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-22375] - [Vault] [Clients] Sort My Items collection to the top of Vault collection filters (#15332)

* WIP - default collection sorting

* apply filtering to popup list filters service.

* add tests. add feature flag checks

* finalize my items collection filters

* fix type error

* re-add service

* re-add comment

* remove unused code

* fix sorting logic

* shorten variable name to fit one line

* fix error

* fix more errors

* abstract logic to vault filter service

* fix test

* export sort as function instead of adding to class

* fix more tests

* add collator arg

* remove ts-ignore. fix type errors

* remove optional param

* fix vault filter service
This commit is contained in:
Jordan Aasen
2025-07-09 08:37:38 -07:00
committed by GitHub
parent e7d5cde105
commit 9f1531a1b2
10 changed files with 244 additions and 61 deletions

View File

@@ -918,6 +918,8 @@ export default class MainBackground {
this.policyService,
this.stateProvider,
this.accountService,
this.configService,
this.i18nService,
);
this.vaultSettingsService = new VaultSettingsService(this.stateProvider);

View File

@@ -404,6 +404,8 @@ const safeProviders: SafeProvider[] = [
PolicyService,
StateProvider,
AccountServiceAbstraction,
ConfigService,
I18nServiceAbstraction,
],
}),
safeProvider({

View File

@@ -5,12 +5,14 @@ import { BehaviorSubject, skipWhile } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import * as vaultFilterSvc from "@bitwarden/angular/vault/vault-filter/services/vault-filter.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";
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 { ProductTierType } from "@bitwarden/common/billing/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
@@ -31,6 +33,14 @@ import {
VaultPopupListFiltersService,
} from "./vault-popup-list-filters.service";
const configService = {
getFeatureFlag$: jest.fn(() => new BehaviorSubject<boolean>(true)),
} as unknown as ConfigService;
jest.mock("@bitwarden/angular/vault/vault-filter/services/vault-filter.service", () => ({
sortDefaultCollections: jest.fn(),
}));
describe("VaultPopupListFiltersService", () => {
let service: VaultPopupListFiltersService;
let _memberOrganizations$ = new BehaviorSubject<Organization[]>([]);
@@ -138,6 +148,10 @@ describe("VaultPopupListFiltersService", () => {
provide: RestrictedItemTypesService,
useValue: restrictedItemTypesService,
},
{
provide: ConfigService,
useValue: configService,
},
],
});
@@ -399,6 +413,29 @@ describe("VaultPopupListFiltersService", () => {
done();
});
});
it("calls vaultFilterService.sortDefaultCollections", (done) => {
const collections = [
{ id: "1234", name: "Default Collection", organizationId: "org1" },
{ id: "5678", name: "Shared Collection", organizationId: "org2" },
] as CollectionView[];
const orgs = [
{ id: "org1", name: "Organization 1" },
{ id: "org2", name: "Organization 2" },
] as Organization[];
createSeededVaultPopupListFiltersService(orgs, collections, [], {});
service.collections$.subscribe(() => {
expect(vaultFilterSvc.sortDefaultCollections).toHaveBeenCalledWith(
collections,
orgs,
i18nService.collator,
);
done();
});
});
});
describe("folders$", () => {
@@ -573,6 +610,8 @@ describe("VaultPopupListFiltersService", () => {
const seededOrganizations: Organization[] = [
{ id: MY_VAULT_ID, name: "Test Org" } as Organization,
{ id: "org1", name: "Default User Collection Org 1" } as Organization,
{ id: "org2", name: "Default User Collection Org 2" } as Organization,
];
const seededCollections: CollectionView[] = [
{
@@ -752,6 +791,7 @@ function createSeededVaultPopupListFiltersService(
accountServiceMock,
viewCacheServiceMock,
restrictedItemTypesServiceMock,
configService,
);
});

View File

@@ -6,6 +6,7 @@ import {
debounceTime,
distinctUntilChanged,
filter,
from,
map,
Observable,
shareReplay,
@@ -17,6 +18,7 @@ import {
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model";
import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.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";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -24,6 +26,8 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
@@ -181,6 +185,7 @@ export class VaultPopupListFiltersService {
private accountService: AccountService,
private viewCacheService: ViewCacheService,
private restrictedItemTypesService: RestrictedItemTypesService,
private configService: ConfigService,
) {
this.filterForm.controls.organization.valueChanges
.pipe(takeUntilDestroyed())
@@ -424,39 +429,47 @@ export class VaultPopupListFiltersService {
/**
* Collection array structured to be directly passed to `ChipSelectComponent`
*/
collections$: Observable<ChipSelectOption<CollectionView>[]> = combineLatest([
this.filters$.pipe(
distinctUntilChanged(
(previousFilter, currentFilter) =>
// Only update the collections when the organizationId filter changes
previousFilter.organization?.id === currentFilter.organization?.id,
collections$: Observable<ChipSelectOption<CollectionView>[]> =
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
combineLatest([
this.filters$.pipe(
distinctUntilChanged((prev, curr) => prev.organization?.id === curr.organization?.id),
),
this.collectionService.decryptedCollections$,
this.organizationService.memberOrganizations$(userId),
this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation),
]),
),
),
this.collectionService.decryptedCollections$,
]).pipe(
map(([filters, allCollections]) => {
const organizationId = filters.organization?.id ?? null;
// When the organization filter is selected, filter out collections that do not belong to the selected organization
const collections =
organizationId === null
? allCollections
: allCollections.filter((c) => c.organizationId === organizationId);
map(([filters, allCollections, orgs, defaultVaultEnabled]) => {
const orgFilterId = filters.organization?.id ?? null;
// When the organization filter is selected, filter out collections that do not belong to the selected organization
const filtered = orgFilterId
? allCollections.filter((c) => c.organizationId === orgFilterId)
: allCollections;
return collections;
}),
switchMap(async (collections) => {
const nestedCollections = await this.collectionService.getAllNested(collections);
return new DynamicTreeNode<CollectionView>({
fullList: collections,
nestedList: nestedCollections,
});
}),
map((collections) =>
collections.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection-shared")),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
if (!defaultVaultEnabled) {
return filtered;
}
return sortDefaultCollections(filtered, orgs, this.i18nService.collator);
}),
switchMap((collections) => {
return from(this.collectionService.getAllNested(collections)).pipe(
map(
(nested) =>
new DynamicTreeNode<CollectionView>({
fullList: collections,
nestedList: nested,
}),
),
);
}),
map((tree) =>
tree.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection-shared")),
),
shareReplay({ bufferSize: 1, refCount: true }),
);
/** Organizations, collection, folders filters. */
allFilters$ = combineLatest([this.organizations$, this.collections$, this.folders$]);

View File

@@ -6,6 +6,8 @@ import { VaultFilterService as BaseVaultFilterService } from "@bitwarden/angular
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -25,6 +27,8 @@ export class VaultFilterService extends BaseVaultFilterService {
policyService: PolicyService,
stateProvider: StateProvider,
accountService: AccountService,
configService: ConfigService,
i18nService: I18nService,
) {
super(
organizationService,
@@ -34,6 +38,8 @@ export class VaultFilterService extends BaseVaultFilterService {
policyService,
stateProvider,
accountService,
configService,
i18nService,
);
this.vaultFilter.myVaultOnly = false;
this.vaultFilter.selectedOrganizationId = null;