1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +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.policyService,
this.stateProvider, this.stateProvider,
this.accountService, this.accountService,
this.configService,
this.i18nService,
); );
this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultSettingsService = new VaultSettingsService(this.stateProvider);

View File

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

View File

@@ -5,12 +5,14 @@ import { BehaviorSubject, skipWhile } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; 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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
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 { ProductTierType } from "@bitwarden/common/billing/enums"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { mockAccountServiceWith } from "@bitwarden/common/spec";
@@ -31,6 +33,14 @@ import {
VaultPopupListFiltersService, VaultPopupListFiltersService,
} from "./vault-popup-list-filters.service"; } 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", () => { describe("VaultPopupListFiltersService", () => {
let service: VaultPopupListFiltersService; let service: VaultPopupListFiltersService;
let _memberOrganizations$ = new BehaviorSubject<Organization[]>([]); let _memberOrganizations$ = new BehaviorSubject<Organization[]>([]);
@@ -138,6 +148,10 @@ describe("VaultPopupListFiltersService", () => {
provide: RestrictedItemTypesService, provide: RestrictedItemTypesService,
useValue: restrictedItemTypesService, useValue: restrictedItemTypesService,
}, },
{
provide: ConfigService,
useValue: configService,
},
], ],
}); });
@@ -399,6 +413,29 @@ describe("VaultPopupListFiltersService", () => {
done(); 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$", () => { describe("folders$", () => {
@@ -573,6 +610,8 @@ describe("VaultPopupListFiltersService", () => {
const seededOrganizations: Organization[] = [ const seededOrganizations: Organization[] = [
{ id: MY_VAULT_ID, name: "Test Org" } as 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[] = [ const seededCollections: CollectionView[] = [
{ {
@@ -752,6 +791,7 @@ function createSeededVaultPopupListFiltersService(
accountServiceMock, accountServiceMock,
viewCacheServiceMock, viewCacheServiceMock,
restrictedItemTypesServiceMock, restrictedItemTypesServiceMock,
configService,
); );
}); });

View File

@@ -6,6 +6,7 @@ import {
debounceTime, debounceTime,
distinctUntilChanged, distinctUntilChanged,
filter, filter,
from,
map, map,
Observable, Observable,
shareReplay, shareReplay,
@@ -17,6 +18,7 @@ import {
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; 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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { import {
@@ -181,6 +185,7 @@ export class VaultPopupListFiltersService {
private accountService: AccountService, private accountService: AccountService,
private viewCacheService: ViewCacheService, private viewCacheService: ViewCacheService,
private restrictedItemTypesService: RestrictedItemTypesService, private restrictedItemTypesService: RestrictedItemTypesService,
private configService: ConfigService,
) { ) {
this.filterForm.controls.organization.valueChanges this.filterForm.controls.organization.valueChanges
.pipe(takeUntilDestroyed()) .pipe(takeUntilDestroyed())
@@ -424,38 +429,46 @@ export class VaultPopupListFiltersService {
/** /**
* Collection array structured to be directly passed to `ChipSelectComponent` * Collection array structured to be directly passed to `ChipSelectComponent`
*/ */
collections$: Observable<ChipSelectOption<CollectionView>[]> = combineLatest([ collections$: Observable<ChipSelectOption<CollectionView>[]> =
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
combineLatest([
this.filters$.pipe( this.filters$.pipe(
distinctUntilChanged( distinctUntilChanged((prev, curr) => prev.organization?.id === curr.organization?.id),
(previousFilter, currentFilter) =>
// Only update the collections when the organizationId filter changes
previousFilter.organization?.id === currentFilter.organization?.id,
),
), ),
this.collectionService.decryptedCollections$, this.collectionService.decryptedCollections$,
]).pipe( this.organizationService.memberOrganizations$(userId),
map(([filters, allCollections]) => { this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation),
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);
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 }), 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;
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. */ /** Organizations, collection, folders filters. */

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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { StateProvider } from "@bitwarden/common/platform/state";
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";
@@ -25,6 +27,8 @@ export class VaultFilterService extends BaseVaultFilterService {
policyService: PolicyService, policyService: PolicyService,
stateProvider: StateProvider, stateProvider: StateProvider,
accountService: AccountService, accountService: AccountService,
configService: ConfigService,
i18nService: I18nService,
) { ) {
super( super(
organizationService, organizationService,
@@ -34,6 +38,8 @@ export class VaultFilterService extends BaseVaultFilterService {
policyService, policyService,
stateProvider, stateProvider,
accountService, accountService,
configService,
i18nService,
); );
this.vaultFilter.myVaultOnly = false; this.vaultFilter.myVaultOnly = false;
this.vaultFilter.selectedOrganizationId = null; this.vaultFilter.selectedOrganizationId = null;

View File

@@ -5,6 +5,7 @@ import { CollectionAdminView, CollectionService } from "@bitwarden/admin-console
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -34,6 +35,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
stateProvider: StateProvider, stateProvider: StateProvider,
collectionService: CollectionService, collectionService: CollectionService,
accountService: AccountService, accountService: AccountService,
configService: ConfigService,
) { ) {
super( super(
organizationService, organizationService,
@@ -44,6 +46,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
stateProvider, stateProvider,
collectionService, collectionService,
accountService, accountService,
configService,
); );
} }

View File

@@ -5,13 +5,20 @@ import {
import { FakeSingleUserState } 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, of, ReplaySubject } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import {
CollectionService,
CollectionType,
CollectionTypes,
CollectionView,
} from "@bitwarden/admin-console/common";
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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -23,6 +30,10 @@ import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/
import { VaultFilterService } from "./vault-filter.service"; import { VaultFilterService } from "./vault-filter.service";
jest.mock("@bitwarden/angular/vault/vault-filter/services/vault-filter.service", () => ({
sortDefaultCollections: jest.fn(() => []),
}));
describe("vault filter service", () => { describe("vault filter service", () => {
let vaultFilterService: VaultFilterService; let vaultFilterService: VaultFilterService;
@@ -39,6 +50,7 @@ describe("vault filter service", () => {
let organizationDataOwnershipPolicy: ReplaySubject<boolean>; let organizationDataOwnershipPolicy: ReplaySubject<boolean>;
let singleOrgPolicy: ReplaySubject<boolean>; let singleOrgPolicy: ReplaySubject<boolean>;
let stateProvider: FakeStateProvider; let stateProvider: FakeStateProvider;
let configService: MockProxy<ConfigService>;
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService; let accountService: FakeAccountService;
@@ -54,6 +66,7 @@ describe("vault filter service", () => {
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
i18nService.collator = new Intl.Collator("en-US"); i18nService.collator = new Intl.Collator("en-US");
collectionService = mock<CollectionService>(); collectionService = mock<CollectionService>();
configService = mock<ConfigService>();
organizations = new ReplaySubject<Organization[]>(1); organizations = new ReplaySubject<Organization[]>(1);
folderViews = new ReplaySubject<FolderView[]>(1); folderViews = new ReplaySubject<FolderView[]>(1);
@@ -62,6 +75,7 @@ describe("vault filter service", () => {
organizationDataOwnershipPolicy = new ReplaySubject<boolean>(1); organizationDataOwnershipPolicy = new ReplaySubject<boolean>(1);
singleOrgPolicy = new ReplaySubject<boolean>(1); singleOrgPolicy = new ReplaySubject<boolean>(1);
configService.getFeatureFlag$.mockReturnValue(of(true));
organizationService.memberOrganizations$.mockReturnValue(organizations); organizationService.memberOrganizations$.mockReturnValue(organizations);
folderService.folderViews$.mockReturnValue(folderViews); folderService.folderViews$.mockReturnValue(folderViews);
collectionService.decryptedCollections$ = collectionViews; collectionService.decryptedCollections$ = collectionViews;
@@ -82,8 +96,10 @@ describe("vault filter service", () => {
stateProvider, stateProvider,
collectionService, collectionService,
accountService, accountService,
configService,
); );
collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS); collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS);
organizations.next([]);
}); });
describe("collapsed filter nodes", () => { describe("collapsed filter nodes", () => {
@@ -285,6 +301,40 @@ describe("vault filter service", () => {
const c3 = c1.children[0]; const c3 = c1.children[0];
expect(c3.parent.node.id).toEqual("id-1"); expect(c3.parent.node.id).toEqual("id-1");
}); });
it.only("calls sortDefaultCollections with the correct args", async () => {
const storedOrgs = [
createOrganization("id-defaultOrg1", "org1"),
createOrganization("id-defaultOrg2", "org2"),
];
organizations.next(storedOrgs);
const storedCollections = [
createCollectionView("id-2", "Collection 2", "org test id"),
createCollectionView("id-1", "Collection 1", "org test id"),
createCollectionView(
"id-3",
"Default User Collection - Org 2",
"id-defaultOrg2",
CollectionTypes.DefaultUserCollection,
),
createCollectionView(
"id-4",
"Default User Collection - Org 1",
"id-defaultOrg1",
CollectionTypes.DefaultUserCollection,
),
];
collectionViews.next(storedCollections);
await firstValueFrom(vaultFilterService.collectionTree$);
expect(vaultFilterSvc.sortDefaultCollections).toHaveBeenCalledWith(
storedCollections,
storedOrgs,
i18nService.collator,
);
});
}); });
}); });
@@ -312,11 +362,17 @@ describe("vault filter service", () => {
return folder; return folder;
} }
function createCollectionView(id: string, name: string, orgId: string): CollectionView { function createCollectionView(
id: string,
name: string,
orgId: string,
type?: CollectionType,
): CollectionView {
const collection = new CollectionView(); const collection = new CollectionView();
collection.id = id; collection.id = id;
collection.name = name; collection.name = name;
collection.organizationId = orgId; collection.organizationId = orgId;
collection.type = type || CollectionTypes.SharedCollection;
return collection; return collection;
} }
}); });

View File

@@ -18,12 +18,15 @@ import {
CollectionService, CollectionService,
CollectionView, CollectionView,
} from "@bitwarden/admin-console/common"; } from "@bitwarden/admin-console/common";
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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
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 { getUserId } from "@bitwarden/common/auth/services/account.service";
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -104,8 +107,14 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
}), }),
); );
collectionTree$: Observable<TreeNode<CollectionFilter>> = this.filteredCollections$.pipe( collectionTree$: Observable<TreeNode<CollectionFilter>> = combineLatest([
map((collections) => this.buildCollectionTree(collections)), this.filteredCollections$,
this.memberOrganizations$,
this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation),
]).pipe(
map(([collections, organizations, defaultCollectionsFlagEnabled]) =>
this.buildCollectionTree(collections, organizations, defaultCollectionsFlagEnabled),
),
); );
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree(); cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree();
@@ -123,6 +132,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
protected stateProvider: StateProvider, protected stateProvider: StateProvider,
protected collectionService: CollectionService, protected collectionService: CollectionService,
protected accountService: AccountService, protected accountService: AccountService,
protected configService: ConfigService,
) {} ) {}
async getCollectionNodeFromTree(id: string) { async getCollectionNodeFromTree(id: string) {
@@ -227,15 +237,22 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
: storedCollections; : storedCollections;
} }
protected buildCollectionTree(collections?: CollectionView[]): TreeNode<CollectionFilter> { protected buildCollectionTree(
collections?: CollectionView[],
orgs?: Organization[],
defaultCollectionsFlagEnabled?: boolean,
): TreeNode<CollectionFilter> {
const headNode = this.getCollectionFilterHead(); const headNode = this.getCollectionFilterHead();
if (!collections) { if (!collections) {
return headNode; return headNode;
} }
const nodes: TreeNode<CollectionFilter>[] = []; const nodes: TreeNode<CollectionFilter>[] = [];
collections
.sort((a, b) => this.i18nService.collator.compare(a.name, b.name)) if (defaultCollectionsFlagEnabled) {
.forEach((c) => { collections = sortDefaultCollections(collections, orgs, this.i18nService.collator);
}
collections.forEach((c) => {
const collectionCopy = new CollectionView() as CollectionFilter; const collectionCopy = new CollectionView() as CollectionFilter;
collectionCopy.id = c.id; collectionCopy.id = c.id;
collectionCopy.organizationId = c.organizationId; collectionCopy.organizationId = c.organizationId;
@@ -244,14 +261,15 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
collectionCopy.groups = c.groups; collectionCopy.groups = c.groups;
collectionCopy.assigned = c.assigned; collectionCopy.assigned = c.assigned;
} }
const parts = const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
}); });
nodes.forEach((n) => { nodes.forEach((n) => {
n.parent = headNode; n.parent = headNode;
headNode.children.push(n); headNode.children.push(n);
}); });
return headNode; return headNode;
} }

View File

@@ -864,6 +864,9 @@
"me": { "me": {
"message": "Me" "message": "Me"
}, },
"myItems": {
"message": "My items"
},
"myVault": { "myVault": {
"message": "My vault" "message": "My vault"
}, },

View File

@@ -1,17 +1,22 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { firstValueFrom, from, map, mergeMap, Observable, switchMap, take } from "rxjs"; import { firstValueFrom, from, map, mergeMap, Observable, switchMap, take } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import {
CollectionService,
CollectionTypes,
CollectionView,
} from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
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 { getUserId } from "@bitwarden/common/auth/services/account.service";
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 { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid"; 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";
@@ -40,6 +45,8 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
protected policyService: PolicyService, protected policyService: PolicyService,
protected stateProvider: StateProvider, protected stateProvider: StateProvider,
protected accountService: AccountService, protected accountService: AccountService,
protected configService: ConfigService,
protected i18nService: I18nService,
) {} ) {}
async storeCollapsedFilterNodes( async storeCollapsedFilterNodes(
@@ -103,12 +110,20 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> { async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
const storedCollections = await this.collectionService.getAllDecrypted(); const storedCollections = await this.collectionService.getAllDecrypted();
let collections: CollectionView[]; const orgs = await this.buildOrganizations();
if (organizationId != null) { const defaulCollectionsFlagEnabled = await this.configService.getFeatureFlag(
collections = storedCollections.filter((c) => c.organizationId === organizationId); FeatureFlag.CreateDefaultLocation,
} else { );
collections = storedCollections;
let collections =
organizationId == null
? storedCollections
: storedCollections.filter((c) => c.organizationId === organizationId);
if (defaulCollectionsFlagEnabled) {
collections = sortDefaultCollections(collections, orgs, this.i18nService.collator);
} }
const nestedCollections = await this.collectionService.getAllNested(collections); const nestedCollections = await this.collectionService.getAllNested(collections);
return new DynamicTreeNode<CollectionView>({ return new DynamicTreeNode<CollectionView>({
fullList: collections, fullList: collections,
@@ -145,7 +160,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
folderCopy.id = f.id; folderCopy.id = f.id;
folderCopy.revisionDate = f.revisionDate; folderCopy.revisionDate = f.revisionDate;
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter); ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, undefined, NestingDelimiter);
}); });
return nodes; return nodes;
} }
@@ -158,3 +173,28 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
return ServiceUtils.getTreeNodeObjectFromList(folders, id) as TreeNode<FolderView>; return ServiceUtils.getTreeNodeObjectFromList(folders, id) as TreeNode<FolderView>;
} }
} }
/**
* Sorts collections with default user collections at the top, sorted by organization name.
* Remaining collections are sorted by name.
* @param collections - The list of collections to sort.
* @param orgs - The list of organizations to use for sorting default user collections.
* @returns Sorted list of collections.
*/
export function sortDefaultCollections(
collections: CollectionView[],
orgs: Organization[] = [],
collator: Intl.Collator,
): CollectionView[] {
const sortedDefaultCollectionTypes = collections
.filter((c) => c.type === CollectionTypes.DefaultUserCollection)
.sort((a, b) => {
const aName = orgs.find((o) => o.id === a.organizationId)?.name ?? a.organizationId;
const bName = orgs.find((o) => o.id === b.organizationId)?.name ?? b.organizationId;
return collator.compare(aName, bName);
});
return [
...sortedDefaultCollectionTypes,
...collections.filter((c) => c.type !== CollectionTypes.DefaultUserCollection),
];
}