mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07: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:
@@ -5,6 +5,7 @@ import { CollectionAdminView, CollectionService } from "@bitwarden/admin-console
|
||||
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";
|
||||
@@ -34,6 +35,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
|
||||
stateProvider: StateProvider,
|
||||
collectionService: CollectionService,
|
||||
accountService: AccountService,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
organizationService,
|
||||
@@ -44,6 +46,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
|
||||
stateProvider,
|
||||
collectionService,
|
||||
accountService,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,20 @@ import {
|
||||
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";
|
||||
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 { 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 { 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 { 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";
|
||||
|
||||
jest.mock("@bitwarden/angular/vault/vault-filter/services/vault-filter.service", () => ({
|
||||
sortDefaultCollections: jest.fn(() => []),
|
||||
}));
|
||||
|
||||
describe("vault filter service", () => {
|
||||
let vaultFilterService: VaultFilterService;
|
||||
|
||||
@@ -39,6 +50,7 @@ describe("vault filter service", () => {
|
||||
let organizationDataOwnershipPolicy: ReplaySubject<boolean>;
|
||||
let singleOrgPolicy: ReplaySubject<boolean>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
@@ -54,6 +66,7 @@ describe("vault filter service", () => {
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
i18nService.collator = new Intl.Collator("en-US");
|
||||
collectionService = mock<CollectionService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
organizations = new ReplaySubject<Organization[]>(1);
|
||||
folderViews = new ReplaySubject<FolderView[]>(1);
|
||||
@@ -62,6 +75,7 @@ describe("vault filter service", () => {
|
||||
organizationDataOwnershipPolicy = new ReplaySubject<boolean>(1);
|
||||
singleOrgPolicy = new ReplaySubject<boolean>(1);
|
||||
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
organizationService.memberOrganizations$.mockReturnValue(organizations);
|
||||
folderService.folderViews$.mockReturnValue(folderViews);
|
||||
collectionService.decryptedCollections$ = collectionViews;
|
||||
@@ -82,8 +96,10 @@ describe("vault filter service", () => {
|
||||
stateProvider,
|
||||
collectionService,
|
||||
accountService,
|
||||
configService,
|
||||
);
|
||||
collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS);
|
||||
organizations.next([]);
|
||||
});
|
||||
|
||||
describe("collapsed filter nodes", () => {
|
||||
@@ -285,6 +301,40 @@ describe("vault filter service", () => {
|
||||
const c3 = c1.children[0];
|
||||
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;
|
||||
}
|
||||
|
||||
function createCollectionView(id: string, name: string, orgId: string): CollectionView {
|
||||
function createCollectionView(
|
||||
id: string,
|
||||
name: string,
|
||||
orgId: string,
|
||||
type?: CollectionType,
|
||||
): CollectionView {
|
||||
const collection = new CollectionView();
|
||||
collection.id = id;
|
||||
collection.name = name;
|
||||
collection.organizationId = orgId;
|
||||
collection.type = type || CollectionTypes.SharedCollection;
|
||||
return collection;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,12 +18,15 @@ import {
|
||||
CollectionService,
|
||||
CollectionView,
|
||||
} 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 { 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 { 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 { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -104,8 +107,14 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
}),
|
||||
);
|
||||
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>> = this.filteredCollections$.pipe(
|
||||
map((collections) => this.buildCollectionTree(collections)),
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>> = combineLatest([
|
||||
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();
|
||||
@@ -123,6 +132,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
protected stateProvider: StateProvider,
|
||||
protected collectionService: CollectionService,
|
||||
protected accountService: AccountService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async getCollectionNodeFromTree(id: string) {
|
||||
@@ -227,31 +237,39 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
: storedCollections;
|
||||
}
|
||||
|
||||
protected buildCollectionTree(collections?: CollectionView[]): TreeNode<CollectionFilter> {
|
||||
protected buildCollectionTree(
|
||||
collections?: CollectionView[],
|
||||
orgs?: Organization[],
|
||||
defaultCollectionsFlagEnabled?: boolean,
|
||||
): TreeNode<CollectionFilter> {
|
||||
const headNode = this.getCollectionFilterHead();
|
||||
if (!collections) {
|
||||
return headNode;
|
||||
}
|
||||
const nodes: TreeNode<CollectionFilter>[] = [];
|
||||
collections
|
||||
.sort((a, b) => this.i18nService.collator.compare(a.name, b.name))
|
||||
.forEach((c) => {
|
||||
const collectionCopy = new CollectionView() as CollectionFilter;
|
||||
collectionCopy.id = c.id;
|
||||
collectionCopy.organizationId = c.organizationId;
|
||||
collectionCopy.icon = "bwi-collection-shared";
|
||||
if (c instanceof CollectionAdminView) {
|
||||
collectionCopy.groups = c.groups;
|
||||
collectionCopy.assigned = c.assigned;
|
||||
}
|
||||
const parts =
|
||||
c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
|
||||
});
|
||||
|
||||
if (defaultCollectionsFlagEnabled) {
|
||||
collections = sortDefaultCollections(collections, orgs, this.i18nService.collator);
|
||||
}
|
||||
|
||||
collections.forEach((c) => {
|
||||
const collectionCopy = new CollectionView() as CollectionFilter;
|
||||
collectionCopy.id = c.id;
|
||||
collectionCopy.organizationId = c.organizationId;
|
||||
collectionCopy.icon = "bwi-collection-shared";
|
||||
if (c instanceof CollectionAdminView) {
|
||||
collectionCopy.groups = c.groups;
|
||||
collectionCopy.assigned = c.assigned;
|
||||
}
|
||||
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
|
||||
});
|
||||
|
||||
nodes.forEach((n) => {
|
||||
n.parent = headNode;
|
||||
headNode.children.push(n);
|
||||
});
|
||||
|
||||
return headNode;
|
||||
}
|
||||
|
||||
|
||||
@@ -864,6 +864,9 @@
|
||||
"me": {
|
||||
"message": "Me"
|
||||
},
|
||||
"myItems": {
|
||||
"message": "My items"
|
||||
},
|
||||
"myVault": {
|
||||
"message": "My vault"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user