mirror of
https://github.com/bitwarden/browser
synced 2026-01-21 11:53:34 +00:00
Desktop/pm 18769/migrate vault filters (#17919)
Migrated vault filters to new v3 vault's navigation * Decoupled existing vault filtering from vault component by using routed params with routed-vault-filter-bridge * Converted vault filters to standalone components * Removed extending filter Base Components from deprecated /libs/angular library and handled logic directly * Moved shared 'models' and 'services' directories from web-vault into /libs/vault
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAdminView,
|
||||
CollectionAccessSelectionView,
|
||||
CollectionDetailsResponse,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionAccessSelectionView, CollectionAdminView } from "../models";
|
||||
|
||||
export abstract class CollectionAdminService {
|
||||
abstract collectionAdminViews$(
|
||||
organizationId: string,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionView,
|
||||
Collection,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { CollectionData, Collection, CollectionView } from "../models";
|
||||
|
||||
export abstract class CollectionService {
|
||||
abstract encryptedCollections$(userId: UserId): Observable<Collection[] | null>;
|
||||
abstract decryptedCollections$(userId: UserId): Observable<CollectionView[]>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Collection } from "./collection";
|
||||
import { Collection } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { BaseCollectionRequest } from "./collection.request";
|
||||
|
||||
export class CollectionWithIdRequest extends BaseCollectionRequest {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
Collection,
|
||||
CollectionTypes,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { makeSymmetricCryptoKey } from "@bitwarden/common/spec";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { Collection, CollectionTypes } from "./collection";
|
||||
import { CollectionData } from "./collection.data";
|
||||
import { CollectionDetailsResponse } from "./collection.response";
|
||||
|
||||
describe("Collection", () => {
|
||||
let data: CollectionData;
|
||||
let encService: MockProxy<EncryptService>;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export * from "./bulk-collection-access.request";
|
||||
export * from "./collection-access-selection.view";
|
||||
export * from "./collection-admin.view";
|
||||
export * from "./collection";
|
||||
export * from "./collection.data";
|
||||
export * from "./collection.view";
|
||||
export * from "./collection.request";
|
||||
export * from "./collection.response";
|
||||
export * from "./collection-with-id.request";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CollectionView, CollectionData } from "@bitwarden/common/admin-console/models/collections";
|
||||
import {
|
||||
COLLECTION_DISK,
|
||||
COLLECTION_MEMORY,
|
||||
@@ -7,8 +8,6 @@ import {
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionData, CollectionView } from "../models";
|
||||
|
||||
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, CollectionId>(
|
||||
COLLECTION_DISK,
|
||||
"collections",
|
||||
|
||||
@@ -5,6 +5,14 @@ import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -13,13 +21,7 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionAdminService, CollectionService } from "../abstractions";
|
||||
import {
|
||||
CollectionData,
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
BulkCollectionAccessRequest,
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
BaseCollectionRequest,
|
||||
UpdateCollectionRequest,
|
||||
CreateCollectionRequest,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { combineLatest, first, firstValueFrom, of, ReplaySubject, takeWhile } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -18,8 +23,6 @@ import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionData, CollectionTypes, CollectionView } from "../models";
|
||||
|
||||
import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state";
|
||||
import { DefaultCollectionService } from "./default-collection.service";
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionView,
|
||||
Collection,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -23,7 +28,6 @@ import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionService } from "../abstractions/collection.service";
|
||||
import { Collection, CollectionData, CollectionView } from "../models";
|
||||
|
||||
import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state";
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
// 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
|
||||
import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
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";
|
||||
|
||||
@@ -3,14 +3,14 @@ import { firstValueFrom, from, map, mergeMap, Observable, switchMap, take } from
|
||||
|
||||
// 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
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
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 {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
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";
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// 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
|
||||
import { CreateCollectionRequest, UpdateCollectionRequest } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
CreateCollectionRequest,
|
||||
UpdateCollectionRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
import { CollectionAccessDetailsResponse, CollectionResponse } from "./collection.response";
|
||||
import { CollectionView } from "./collection.view";
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
CollectionType,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionType, CollectionTypes } from "./collection";
|
||||
import { CollectionDetailsResponse } from "./collection.response";
|
||||
|
||||
export class CollectionData {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
CollectionType,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { SelectionReadOnlyResponse } from "@bitwarden/common/admin-console/models/response/selection-read-only.response";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionType, CollectionTypes } from "./collection";
|
||||
|
||||
export class CollectionResponse extends BaseResponse {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import Domain from "@bitwarden/common/platform/models/domain/domain-base";
|
||||
@@ -5,7 +6,6 @@ import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { CollectionData } from "./collection.data";
|
||||
import { CollectionView } from "./collection.view";
|
||||
|
||||
export const CollectionTypes = {
|
||||
SharedCollection: 0,
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./collection-access-selection.view";
|
||||
export * from "./collection-admin.view";
|
||||
export * from "./collection.view";
|
||||
export * from "./collection.response";
|
||||
export * from "./collection";
|
||||
export * from "./collection.data";
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// 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
|
||||
import { CollectionResponse } from "@bitwarden/admin-console/common";
|
||||
import { CollectionResponse } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CipherResponse } from "../../../vault/models/response/cipher.response";
|
||||
|
||||
120
libs/common/src/admin-console/utils/collection-utils.spec.ts
Normal file
120
libs/common/src/admin-console/utils/collection-utils.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils";
|
||||
|
||||
describe("CollectionUtils Service", () => {
|
||||
describe("getNestedCollectionTree", () => {
|
||||
it("should return collections properly sorted if provided out of order", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
const childCollection = new CollectionView({
|
||||
name: "Parent/Child",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
collections.push(childCollection);
|
||||
collections.push(parentCollection);
|
||||
|
||||
// Act
|
||||
const result = getNestedCollectionTree(collections);
|
||||
|
||||
// Assert
|
||||
expect(result[0].node.name).toBe("Parent");
|
||||
expect(result[0].children[0].node.name).toBe("Child");
|
||||
});
|
||||
|
||||
it("should return an empty array if no collections are provided", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
// Act
|
||||
const result = getNestedCollectionTree(collections);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlatCollectionTree", () => {
|
||||
it("should flatten a tree node with no children", () => {
|
||||
// Arrange
|
||||
const collection = new CollectionView({
|
||||
name: "Test Collection",
|
||||
id: "test-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [
|
||||
new TreeNode<CollectionView>(collection, {} as TreeNode<CollectionView>),
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe(collection);
|
||||
});
|
||||
|
||||
it("should flatten a tree node with children", () => {
|
||||
// Arrange
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
id: "parent-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child1Collection = new CollectionView({
|
||||
name: "Child 1",
|
||||
id: "child1-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child2Collection = new CollectionView({
|
||||
name: "Child 2",
|
||||
id: "child2-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const grandchildCollection = new CollectionView({
|
||||
name: "Grandchild",
|
||||
id: "grandchild-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const parentNode = new TreeNode<CollectionView>(
|
||||
parentCollection,
|
||||
{} as TreeNode<CollectionView>,
|
||||
);
|
||||
const child1Node = new TreeNode<CollectionView>(child1Collection, parentNode);
|
||||
const child2Node = new TreeNode<CollectionView>(child2Collection, parentNode);
|
||||
const grandchildNode = new TreeNode<CollectionView>(grandchildCollection, child1Node);
|
||||
|
||||
parentNode.children = [child1Node, child2Node];
|
||||
child1Node.children = [grandchildNode];
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [parentNode];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(4);
|
||||
expect(result[0]).toBe(parentCollection);
|
||||
expect(result).toContain(child1Collection);
|
||||
expect(result).toContain(child2Collection);
|
||||
expect(result).toContain(grandchildCollection);
|
||||
});
|
||||
});
|
||||
});
|
||||
87
libs/common/src/admin-console/utils/collection-utils.ts
Normal file
87
libs/common/src/admin-console/utils/collection-utils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
CollectionView,
|
||||
NestingDelimiter,
|
||||
CollectionAdminView,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
|
||||
export function getNestedCollectionTree(
|
||||
collections: CollectionAdminView[],
|
||||
): TreeNode<CollectionAdminView>[];
|
||||
export function getNestedCollectionTree(collections: CollectionView[]): TreeNode<CollectionView>[];
|
||||
export function getNestedCollectionTree(
|
||||
collections: (CollectionView | CollectionAdminView)[],
|
||||
): TreeNode<CollectionView | CollectionAdminView>[] {
|
||||
if (!collections) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
|
||||
// modifies the names of collections.
|
||||
// These changes risk affecting collections store in StateService.
|
||||
const clonedCollections: CollectionView[] | CollectionAdminView[] = collections
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(cloneCollection);
|
||||
|
||||
const all: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
const groupedByOrg = new Map<OrganizationId, (CollectionView | CollectionAdminView)[]>();
|
||||
clonedCollections.map((c) => {
|
||||
const key = c.organizationId;
|
||||
(groupedByOrg.get(key) ?? groupedByOrg.set(key, []).get(key)!).push(c);
|
||||
});
|
||||
for (const group of groupedByOrg.values()) {
|
||||
const nodes: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
for (const c of group) {
|
||||
const parts = c.name ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, c, undefined, NestingDelimiter);
|
||||
}
|
||||
all.push(...nodes);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
export function cloneCollection(collection: CollectionView): CollectionView;
|
||||
export function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
export function cloneCollection(
|
||||
collection: CollectionView | CollectionAdminView,
|
||||
): CollectionView | CollectionAdminView {
|
||||
let cloned;
|
||||
|
||||
if (collection instanceof CollectionAdminView) {
|
||||
cloned = Object.assign(
|
||||
new CollectionAdminView({ ...collection, name: collection.name }),
|
||||
collection,
|
||||
);
|
||||
} else {
|
||||
cloned = Object.assign(
|
||||
new CollectionView({ ...collection, name: collection.name }),
|
||||
collection,
|
||||
);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
export function getFlatCollectionTree(nodes: TreeNode<CollectionView>[]): CollectionView[];
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionView | CollectionAdminView>[],
|
||||
): (CollectionView | CollectionAdminView)[] {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return nodes.flatMap((node) => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return [node.node];
|
||||
}
|
||||
|
||||
const children = getFlatCollectionTree(node.children);
|
||||
return [node.node, ...children];
|
||||
});
|
||||
}
|
||||
1
libs/common/src/admin-console/utils/index.ts
Normal file
1
libs/common/src/admin-console/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./collection-utils";
|
||||
@@ -1,8 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// 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
|
||||
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionView,
|
||||
Collection as CollectionDomain,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionExport } from "./collection.export";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// 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
|
||||
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionView,
|
||||
Collection as CollectionDomain,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { CollectionId, emptyGuid, OrganizationId } from "../../types/guid";
|
||||
|
||||
@@ -4,11 +4,11 @@ import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionData,
|
||||
CollectionDetailsResponse,
|
||||
CollectionService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// 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
|
||||
import { CollectionDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { PolicyResponse } from "../../admin-console/models/response/policy.response";
|
||||
import { UserDecryptionResponse } from "../../key-management/models/response/user-decryption.response";
|
||||
|
||||
@@ -4,16 +4,15 @@ import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CreateCollectionRequest, UpdateCollectionRequest } from "@bitwarden/admin-console/common";
|
||||
// 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
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
CreateCollectionRequest,
|
||||
UpdateCollectionRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// 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
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
|
||||
@@ -3,8 +3,9 @@ import { Observable, firstValueFrom, of } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@@ -29,15 +29,15 @@ import { combineLatestWith, filter, map, switchMap, takeUntil } from "rxjs/opera
|
||||
|
||||
// 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
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
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 {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
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";
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import * as papa from "papaparse";
|
||||
|
||||
// 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
|
||||
import { Collection, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView, Collection } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { filter, firstValueFrom } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Collection } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { FieldType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export abstract class ImportCollectionServiceAbstraction {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { Importer } from "../importers/importer";
|
||||
|
||||
@@ -2,11 +2,11 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
// 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
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
|
||||
@@ -4,12 +4,11 @@ import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionService, CollectionWithIdRequest } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionWithIdRequest,
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import * as papa from "papaparse";
|
||||
import { filter, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionData,
|
||||
Collection,
|
||||
CollectionDetailsResponse,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
CollectionDetailsResponse,
|
||||
Collection,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
|
||||
44
libs/vault/src/abstractions/vault-filter.service.ts
Normal file
44
libs/vault/src/abstractions/vault-filter.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAdminView,
|
||||
CollectionView,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
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";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "../models/vault-filter.type";
|
||||
|
||||
export abstract class VaultFilterService {
|
||||
collapsedFilterNodes$: Observable<Set<string>>;
|
||||
filteredFolders$: Observable<FolderView[]>;
|
||||
filteredCollections$: Observable<CollectionView[]>;
|
||||
organizationTree$: Observable<TreeNode<OrganizationFilter>>;
|
||||
folderTree$: Observable<TreeNode<FolderFilter>>;
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>>;
|
||||
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>>;
|
||||
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
|
||||
abstract reloadCollections?: (collections: CollectionAdminView[]) => void;
|
||||
abstract clearOrganizationFilter: () => void;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
@@ -12,14 +12,12 @@ import {
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { action } from "storybook/actions";
|
||||
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { NudgeStatus, NudgesService } from "@bitwarden/angular/vault";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
|
||||
@@ -5,11 +5,13 @@ import { By } from "@angular/platform-browser";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionType, CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionType,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
|
||||
@@ -6,13 +6,14 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
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";
|
||||
|
||||
@@ -5,9 +5,10 @@ import { combineLatest, of, switchMap, map, catchError, from, Observable, startW
|
||||
|
||||
// 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
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { isCardExpired } from "@bitwarden/common/autofill/utils";
|
||||
|
||||
@@ -4,9 +4,7 @@ import { By } from "@angular/platform-browser";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
// 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
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
|
||||
@@ -6,10 +6,12 @@ import { Component, computed, input, signal } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { fromEvent, map, startWith } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -5,12 +5,12 @@ import { of } from "rxjs";
|
||||
|
||||
// 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
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
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";
|
||||
|
||||
@@ -26,17 +26,17 @@ import {
|
||||
|
||||
// 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
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
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";
|
||||
|
||||
@@ -39,3 +39,14 @@ export * from "./abstractions/vault-items-transfer.service";
|
||||
export * from "./services/default-vault-items-transfer.service";
|
||||
export * from "./services/default-change-login-password.service";
|
||||
export * from "./services/archive-cipher-utilities.service";
|
||||
|
||||
export * from "./models/vault-filter.type";
|
||||
export * from "./models/vault-filter.model";
|
||||
export * from "./models/routed-vault-filter.model";
|
||||
export * from "./models/routed-vault-filter-bridge.model";
|
||||
export * from "./models/vault-filter-section.type";
|
||||
export * from "./models/filter-function";
|
||||
export { VaultFilterService as VaultFilterServiceAbstraction } from "./abstractions/vault-filter.service";
|
||||
export * from "./services/vault-filter.service";
|
||||
export * from "./services/routed-vault-filter.service";
|
||||
export * from "./services/routed-vault-filter-bridge.service";
|
||||
|
||||
231
libs/vault/src/models/filter-function.spec.ts
Normal file
231
libs/vault/src/models/filter-function.spec.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { Unassigned } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { createFilterFunction } from "./filter-function";
|
||||
import { All } from "./routed-vault-filter.model";
|
||||
|
||||
describe("createFilter", () => {
|
||||
describe("given a generic cipher", () => {
|
||||
it("should return true when no filter is applied", () => {
|
||||
const cipher = createCipher();
|
||||
const filterFunction = createFilterFunction({});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a favorite cipher", () => {
|
||||
const cipher = createCipher({ favorite: true });
|
||||
|
||||
it("should return true when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ type: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ type: "trash" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a deleted cipher", () => {
|
||||
const cipher = createCipher({ deletedDate: new Date() });
|
||||
|
||||
it("should return true when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ type: "trash" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ type: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when type is not specified in filter", () => {
|
||||
const filterFunction = createFilterFunction({});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with type", () => {
|
||||
it("should return true when filter matches cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Identity });
|
||||
const filterFunction = createFilterFunction({ type: "identity" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Card });
|
||||
const filterFunction = createFilterFunction({ type: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with folder id", () => {
|
||||
it("should return true when filter matches folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({ folderId: "folderId" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({ folderId: "differentFolderId" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher without folder", () => {
|
||||
const cipher = createCipher({ folderId: null });
|
||||
|
||||
it("should return true when filtering on unassigned folder", () => {
|
||||
const filterFunction = createFilterFunction({ folderId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an organizational cipher (with organization and collections)", () => {
|
||||
const cipher = createCipher({
|
||||
organizationId: "organizationId",
|
||||
collectionIds: ["collectionId", "anotherId"],
|
||||
});
|
||||
|
||||
it("should return true when filter matches collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
collectionId: "collectionId" as CollectionId,
|
||||
organizationId: "organizationId" as OrganizationId,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
collectionId: "nonMatchingCollectionId" as CollectionId,
|
||||
organizationId: "organizationId" as OrganizationId,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
organizationId: "nonMatchingOrganizationId" as OrganizationId,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering for my vault only", () => {
|
||||
const filterFunction = createFilterFunction({ organizationId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering by All Collections", () => {
|
||||
const filterFunction = createFilterFunction({ collectionId: All });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an unassigned organizational cipher (with organization, without collection)", () => {
|
||||
const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] });
|
||||
|
||||
it("should return true when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({ collectionId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when filter matches organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
organizationId: "organizationId" as OrganizationId,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an individual cipher (without organization or collection)", () => {
|
||||
const cipher = createCipher({ organizationId: null, collectionIds: [] });
|
||||
|
||||
it("should return false when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({ collectionId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when filtering for my vault only", () => {
|
||||
const cipher = createCipher({ organizationId: null });
|
||||
const filterFunction = createFilterFunction({ organizationId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
const cipher = new CipherView();
|
||||
|
||||
cipher.favorite = options.favorite ?? false;
|
||||
cipher.deletedDate = options.deletedDate;
|
||||
cipher.type = options.type ?? CipherType.Login;
|
||||
cipher.folderId = options.folderId;
|
||||
cipher.collectionIds = options.collectionIds;
|
||||
cipher.organizationId = options.organizationId;
|
||||
|
||||
return cipher;
|
||||
}
|
||||
107
libs/vault/src/models/filter-function.ts
Normal file
107
libs/vault/src/models/filter-function.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Unassigned } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
||||
|
||||
export type FilterFunction = (cipher: CipherViewLike) => boolean;
|
||||
|
||||
export function createFilterFunction(
|
||||
filter: RoutedVaultFilterModel,
|
||||
archiveEnabled?: boolean,
|
||||
): FilterFunction {
|
||||
return (cipher) => {
|
||||
const type = CipherViewLikeUtils.getType(cipher);
|
||||
const isDeleted = CipherViewLikeUtils.isDeleted(cipher);
|
||||
|
||||
if (filter.type === "favorites" && !cipher.favorite) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "card" && type !== CipherType.Card) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "identity" && type !== CipherType.Identity) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "login" && type !== CipherType.Login) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "note" && type !== CipherType.SecureNote) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "sshKey" && type !== CipherType.SshKey) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "trash" && !isDeleted) {
|
||||
return false;
|
||||
}
|
||||
// Hide trash unless explicitly selected
|
||||
if (filter.type !== "trash" && isDeleted) {
|
||||
return false;
|
||||
}
|
||||
// Archive filter logic is only applied if the feature flag is enabled
|
||||
if (archiveEnabled) {
|
||||
if (filter.type === "archive" && !CipherViewLikeUtils.isArchived(cipher)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter.type !== "archive" &&
|
||||
filter.type !== "trash" &&
|
||||
CipherViewLikeUtils.isArchived(cipher)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// No folder
|
||||
if (filter.folderId === Unassigned && cipher.folderId != null) {
|
||||
return false;
|
||||
}
|
||||
// Folder
|
||||
if (
|
||||
filter.folderId !== undefined &&
|
||||
filter.folderId !== All &&
|
||||
filter.folderId !== Unassigned &&
|
||||
cipher.folderId !== filter.folderId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// All collections (top level)
|
||||
if (filter.collectionId === All) {
|
||||
return false;
|
||||
}
|
||||
// Unassigned
|
||||
if (
|
||||
filter.collectionId === Unassigned &&
|
||||
(cipher.organizationId == null ||
|
||||
(cipher.collectionIds != null && cipher.collectionIds.length > 0))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// Collection
|
||||
if (
|
||||
filter.collectionId !== undefined &&
|
||||
filter.collectionId !== All &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
(cipher.collectionIds == null || !cipher.collectionIds.includes(filter.collectionId as any))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// My Vault
|
||||
if (filter.organizationId === Unassigned && cipher.organizationId != null) {
|
||||
return false;
|
||||
}
|
||||
// Organization
|
||||
else if (
|
||||
filter.organizationId !== undefined &&
|
||||
filter.organizationId !== Unassigned &&
|
||||
cipher.organizationId !== filter.organizationId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
172
libs/vault/src/models/routed-vault-filter-bridge.model.ts
Normal file
172
libs/vault/src/models/routed-vault-filter-bridge.model.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Unassigned } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { RoutedVaultFilterBridgeService } from "../services/routed-vault-filter-bridge.service";
|
||||
|
||||
import {
|
||||
All,
|
||||
isRoutedVaultFilterItemType,
|
||||
RoutedVaultFilterItemType,
|
||||
RoutedVaultFilterModel,
|
||||
} from "./routed-vault-filter.model";
|
||||
import { VaultFilter, VaultFilterFunction } from "./vault-filter.model";
|
||||
import {
|
||||
OrganizationFilter,
|
||||
CipherTypeFilter,
|
||||
FolderFilter,
|
||||
CollectionFilter,
|
||||
CipherStatus,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
/**
|
||||
* This file is part of a layer that is used to temporary bridge between URL filtering and the old state-in-code method.
|
||||
* This should be removed after we have refactored the {@link VaultItemsComponent} and introduced vertical navigation
|
||||
* (which will refactor the {@link VaultFilterComponent}).
|
||||
*
|
||||
* This model supplies legacy code with the old state-in-code models saved as tree nodes.
|
||||
* It can also receive requests to select a new tree node by using setters.
|
||||
* However instead of just replacing the tree node models, it requests a URL navigation,
|
||||
* thereby bridging between legacy and URL filtering.
|
||||
*/
|
||||
export class RoutedVaultFilterBridge implements VaultFilter {
|
||||
constructor(
|
||||
private routedFilter: RoutedVaultFilterModel,
|
||||
private legacyFilter: VaultFilter,
|
||||
private bridgeService: RoutedVaultFilterBridgeService,
|
||||
) {}
|
||||
get collectionBreadcrumbs(): TreeNode<CollectionFilter>[] {
|
||||
return this.legacyFilter.collectionBreadcrumbs;
|
||||
}
|
||||
get isCollectionSelected(): boolean {
|
||||
return this.legacyFilter.isCollectionSelected;
|
||||
}
|
||||
get isUnassignedCollectionSelected(): boolean {
|
||||
return this.legacyFilter.isUnassignedCollectionSelected;
|
||||
}
|
||||
get isMyVaultSelected(): boolean {
|
||||
return this.legacyFilter.isMyVaultSelected;
|
||||
}
|
||||
get selectedOrganizationNode(): TreeNode<OrganizationFilter> {
|
||||
return this.legacyFilter.selectedOrganizationNode;
|
||||
}
|
||||
set selectedOrganizationNode(value: TreeNode<OrganizationFilter>) {
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
organizationId: value?.node.id === "MyVault" ? Unassigned : value?.node.id,
|
||||
folderId: undefined,
|
||||
collectionId: undefined,
|
||||
});
|
||||
}
|
||||
get selectedCipherTypeNode(): TreeNode<CipherTypeFilter> {
|
||||
return this.legacyFilter.selectedCipherTypeNode;
|
||||
}
|
||||
set selectedCipherTypeNode(value: TreeNode<CipherTypeFilter>) {
|
||||
let type: RoutedVaultFilterItemType | undefined;
|
||||
|
||||
if (value?.node.id === "AllItems" && this.routedFilter.organizationIdParamType === "path") {
|
||||
type = All;
|
||||
} else if (
|
||||
value?.node.id === "AllItems" &&
|
||||
this.routedFilter.organizationIdParamType === "query"
|
||||
) {
|
||||
type = undefined;
|
||||
} else if (isRoutedVaultFilterItemType(value?.node.id)) {
|
||||
type = value?.node.id;
|
||||
}
|
||||
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
type,
|
||||
folderId: undefined,
|
||||
collectionId: undefined,
|
||||
});
|
||||
}
|
||||
get selectedFolderNode(): TreeNode<FolderFilter> {
|
||||
return this.legacyFilter.selectedFolderNode;
|
||||
}
|
||||
set selectedFolderNode(value: TreeNode<FolderFilter>) {
|
||||
const folderId = value != null && value.node.id === null ? Unassigned : value?.node.id;
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
folderId,
|
||||
type: undefined,
|
||||
collectionId: undefined,
|
||||
});
|
||||
}
|
||||
get selectedCollectionNode(): TreeNode<CollectionFilter> {
|
||||
return this.legacyFilter.selectedCollectionNode;
|
||||
}
|
||||
set selectedCollectionNode(value: TreeNode<CollectionFilter>) {
|
||||
let collectionId: CollectionId | All | Unassigned | undefined;
|
||||
|
||||
if (value != null && value.node.id === null) {
|
||||
collectionId = Unassigned;
|
||||
} else if (
|
||||
value?.node.id === "AllCollections" &&
|
||||
this.routedFilter.organizationIdParamType === "path"
|
||||
) {
|
||||
collectionId = undefined;
|
||||
} else if (
|
||||
value?.node.id === "AllCollections" &&
|
||||
this.routedFilter.organizationIdParamType === "query"
|
||||
) {
|
||||
collectionId = All;
|
||||
} else {
|
||||
collectionId = value?.node.id;
|
||||
}
|
||||
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
collectionId,
|
||||
type: undefined,
|
||||
folderId: undefined,
|
||||
});
|
||||
}
|
||||
get isFavorites(): boolean {
|
||||
return this.legacyFilter.isFavorites;
|
||||
}
|
||||
get isDeleted(): boolean {
|
||||
return this.legacyFilter.isDeleted;
|
||||
}
|
||||
get isArchived(): boolean {
|
||||
return this.legacyFilter.isArchived;
|
||||
}
|
||||
get organizationId(): string {
|
||||
return this.legacyFilter.organizationId;
|
||||
}
|
||||
get cipherType(): CipherType {
|
||||
return this.legacyFilter.cipherType;
|
||||
}
|
||||
get cipherStatus(): CipherStatus {
|
||||
return this.legacyFilter.cipherStatus;
|
||||
}
|
||||
get cipherTypeId(): string {
|
||||
return this.legacyFilter.cipherTypeId;
|
||||
}
|
||||
get folderId(): string {
|
||||
return this.legacyFilter.folderId;
|
||||
}
|
||||
get collectionId(): string {
|
||||
return this.legacyFilter.collectionId;
|
||||
}
|
||||
resetFilter(): void {
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
collectionId: undefined,
|
||||
folderId: undefined,
|
||||
organizationId:
|
||||
this.routedFilter.organizationIdParamType === "path"
|
||||
? this.routedFilter.organizationId
|
||||
: undefined,
|
||||
type: undefined,
|
||||
});
|
||||
}
|
||||
resetOrganization(): void {
|
||||
this.bridgeService.navigate({ ...this.routedFilter, organizationId: undefined });
|
||||
}
|
||||
buildFilter(): VaultFilterFunction {
|
||||
return this.legacyFilter.buildFilter();
|
||||
}
|
||||
}
|
||||
36
libs/vault/src/models/routed-vault-filter.model.ts
Normal file
36
libs/vault/src/models/routed-vault-filter.model.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Unassigned } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
/**
|
||||
* A constant used to represent viewing "all" of a particular filter.
|
||||
*/
|
||||
export const All = "all";
|
||||
export type All = typeof All;
|
||||
|
||||
// TODO: Remove `All` when moving to vertical navigation.
|
||||
const itemTypes = [
|
||||
"favorites",
|
||||
"login",
|
||||
"card",
|
||||
"identity",
|
||||
"note",
|
||||
"sshKey",
|
||||
"archive",
|
||||
"trash",
|
||||
All,
|
||||
] as const;
|
||||
|
||||
export type RoutedVaultFilterItemType = (typeof itemTypes)[number];
|
||||
|
||||
export function isRoutedVaultFilterItemType(value: unknown): value is RoutedVaultFilterItemType {
|
||||
return itemTypes.includes(value as any);
|
||||
}
|
||||
|
||||
export interface RoutedVaultFilterModel {
|
||||
collectionId?: CollectionId | All | Unassigned;
|
||||
folderId?: string;
|
||||
organizationId?: OrganizationId | Unassigned;
|
||||
type?: RoutedVaultFilterItemType;
|
||||
|
||||
organizationIdParamType?: "path" | "query";
|
||||
}
|
||||
64
libs/vault/src/models/vault-filter-section.type.ts
Normal file
64
libs/vault/src/models/vault-filter-section.type.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
export type VaultFilterType =
|
||||
| OrganizationFilter
|
||||
| CipherTypeFilter
|
||||
| FolderFilter
|
||||
| CollectionFilter;
|
||||
|
||||
export const VaultFilterLabel = {
|
||||
OrganizationFilter: "organizationFilter",
|
||||
TypeFilter: "typeFilter",
|
||||
FolderFilter: "folderFilter",
|
||||
CollectionFilter: "collectionFilter",
|
||||
ArchiveFilter: "archiveFilter",
|
||||
TrashFilter: "trashFilter",
|
||||
} as const;
|
||||
|
||||
type VaultFilterLabel = UnionOfValues<typeof VaultFilterLabel>;
|
||||
|
||||
export type VaultFilterSection = {
|
||||
data$: Observable<TreeNode<VaultFilterType>>;
|
||||
header: {
|
||||
showHeader: boolean;
|
||||
isSelectable: boolean;
|
||||
};
|
||||
action: (filterNode: TreeNode<VaultFilterType>) => Promise<void>;
|
||||
edit?: {
|
||||
filterName: string;
|
||||
action: (filter: VaultFilterType) => void;
|
||||
};
|
||||
add?: {
|
||||
text: string;
|
||||
route?: string;
|
||||
action?: () => void;
|
||||
};
|
||||
options?: {
|
||||
component: any;
|
||||
};
|
||||
divider?: boolean;
|
||||
premiumOptions?: {
|
||||
/** When true, the premium badge will show on the filter for non-premium users. */
|
||||
showBadgeForNonPremium?: true;
|
||||
/**
|
||||
* Action to be called instead of applying the filter.
|
||||
* Useful when the user does not have access to a filter (e.g., premium feature)
|
||||
* and custom behavior is needed when invoking the filter.
|
||||
*/
|
||||
blockFilterAction?: () => Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
export type VaultFilterList = {
|
||||
[key in VaultFilterLabel]?: VaultFilterSection;
|
||||
};
|
||||
338
libs/vault/src/models/vault-filter.model.spec.ts
Normal file
338
libs/vault/src/models/vault-filter.model.spec.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { VaultFilter } from "./vault-filter.model";
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
describe("VaultFilter", () => {
|
||||
describe("filterFunction", () => {
|
||||
const allCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "AllItems",
|
||||
name: "allItems",
|
||||
type: "all",
|
||||
icon: "",
|
||||
},
|
||||
null,
|
||||
);
|
||||
const favoriteCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "favorites",
|
||||
name: "favorites",
|
||||
type: "favorites",
|
||||
icon: "bwi-star",
|
||||
},
|
||||
null,
|
||||
);
|
||||
const identityCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "identity",
|
||||
name: "identity",
|
||||
type: CipherType.Identity,
|
||||
icon: "bwi-id-card",
|
||||
},
|
||||
null,
|
||||
);
|
||||
const trashFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "trash",
|
||||
name: "trash",
|
||||
type: "trash",
|
||||
icon: "bwi-trash",
|
||||
},
|
||||
null,
|
||||
);
|
||||
describe("generic cipher", () => {
|
||||
it("should return true when no filter is applied", () => {
|
||||
const cipher = createCipher();
|
||||
const filterFunction = createFilterFunction({});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a favorite cipher", () => {
|
||||
const cipher = createCipher({ favorite: true });
|
||||
|
||||
it("should return true when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ selectedCipherTypeNode: allCiphersFilter });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a deleted cipher", () => {
|
||||
const cipher = createCipher({ deletedDate: new Date() });
|
||||
|
||||
it("should return true when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCipherTypeNode: favoriteCiphersFilter,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with type", () => {
|
||||
it("should return true when filter matches cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Identity });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCipherTypeNode: identityCiphersFilter,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Card });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCipherTypeNode: identityCiphersFilter,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with folder id", () => {
|
||||
it("should return true when filter matches folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolderNode: createFolderFilterNode({ id: "folderId" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolderNode: createFolderFilterNode({ id: "differentFolderId" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher without folder", () => {
|
||||
const cipher = createCipher({ folderId: null });
|
||||
|
||||
it("should return true when filtering on unassigned folder", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolderNode: createFolderFilterNode({ id: null }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an organizational cipher (with organization and collections)", () => {
|
||||
const cipher = createCipher({
|
||||
organizationId: "organizationId",
|
||||
collectionIds: ["collectionId", "anotherId"],
|
||||
});
|
||||
|
||||
it("should return true when filter matches collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({
|
||||
id: "collectionId" as CollectionId,
|
||||
organizationId: "organizationId" as OrganizationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({
|
||||
id: "nonMatchingCollectionId" as CollectionId,
|
||||
organizationId: "organizationId" as OrganizationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({
|
||||
id: "nonMatchingOrganizationId" as OrganizationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering for my vault only", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({
|
||||
id: "MyVault" as OrganizationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering by All Collections", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({
|
||||
id: "AllCollections" as CollectionId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an unassigned organizational cipher (with organization, without collection)", () => {
|
||||
const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] });
|
||||
|
||||
it("should return true when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({ id: null }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when filter matches organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({
|
||||
id: "organizationId" as OrganizationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an individual cipher (without organization or collection)", () => {
|
||||
const cipher = createCipher({ organizationId: null, collectionIds: [] });
|
||||
|
||||
it("should return false when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({ id: null }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when filtering for my vault only", () => {
|
||||
const cipher = createCipher({ organizationId: null });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({
|
||||
id: "MyVault" as OrganizationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFilterFunction(options: Partial<VaultFilter> = {}) {
|
||||
return new VaultFilter(options).buildFilter();
|
||||
}
|
||||
|
||||
function createOrganizationFilterNode(
|
||||
options: Partial<OrganizationFilter>,
|
||||
): TreeNode<OrganizationFilter> {
|
||||
const org = new Organization() as OrganizationFilter;
|
||||
org.id = options.id;
|
||||
org.icon = options.icon ?? "";
|
||||
return new TreeNode<OrganizationFilter>(org, null);
|
||||
}
|
||||
|
||||
function createFolderFilterNode(options: Partial<FolderFilter>): TreeNode<FolderFilter> {
|
||||
const folder = new FolderView() as FolderFilter;
|
||||
folder.id = options.id;
|
||||
folder.name = options.name;
|
||||
folder.icon = options.icon ?? "";
|
||||
folder.revisionDate = options.revisionDate ?? new Date();
|
||||
return new TreeNode<FolderFilter>(folder, null);
|
||||
}
|
||||
|
||||
function createCollectionFilterNode(
|
||||
options: Partial<CollectionFilter>,
|
||||
): TreeNode<CollectionFilter> {
|
||||
const collection = new CollectionView({
|
||||
name: options.name ?? "Test Name",
|
||||
id: options.id ?? null,
|
||||
organizationId: options.organizationId ?? ("Org Id" as OrganizationId),
|
||||
}) as CollectionFilter;
|
||||
return new TreeNode<CollectionFilter>(collection, {} as TreeNode<CollectionFilter>);
|
||||
}
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
const cipher = new CipherView();
|
||||
|
||||
cipher.favorite = options.favorite ?? false;
|
||||
cipher.deletedDate = options.deletedDate;
|
||||
cipher.type = options.type;
|
||||
cipher.folderId = options.folderId;
|
||||
cipher.collectionIds = options.collectionIds;
|
||||
cipher.organizationId = options.organizationId;
|
||||
|
||||
return cipher;
|
||||
}
|
||||
178
libs/vault/src/models/vault-filter.model.ts
Normal file
178
libs/vault/src/models/vault-filter.model.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CipherType, isCipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import {
|
||||
CipherStatus,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
export type VaultFilterFunction = (cipher: CipherView) => boolean;
|
||||
|
||||
// TODO: Replace shared VaultFilter Model with this one and
|
||||
// refactor browser and desktop code to use this model.
|
||||
export class VaultFilter {
|
||||
selectedOrganizationNode: TreeNode<OrganizationFilter>;
|
||||
selectedCipherTypeNode: TreeNode<CipherTypeFilter>;
|
||||
selectedFolderNode: TreeNode<FolderFilter>;
|
||||
selectedCollectionNode: TreeNode<CollectionFilter>;
|
||||
|
||||
/**
|
||||
* A list of collection filters that form a chain from the organization root to currently selected collection.
|
||||
* To be used when rendering a breadcrumb UI for navigating the collection hierarchy.
|
||||
* Begins from the organization root and excludes the currently selected collection.
|
||||
*/
|
||||
get collectionBreadcrumbs(): TreeNode<CollectionFilter>[] {
|
||||
if (!this.isCollectionSelected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const collections = [this.selectedCollectionNode];
|
||||
while (collections[collections.length - 1].parent != undefined) {
|
||||
collections.push(collections[collections.length - 1].parent);
|
||||
}
|
||||
|
||||
return collections.slice(1).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* The vault is filtered by a specific collection
|
||||
*/
|
||||
get isCollectionSelected(): boolean {
|
||||
return (
|
||||
this.selectedCollectionNode != null &&
|
||||
this.selectedCollectionNode.node.id !== "AllCollections"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The vault is filtered by the "Unassigned" collection
|
||||
*/
|
||||
get isUnassignedCollectionSelected(): boolean {
|
||||
return this.selectedCollectionNode != null && this.selectedCollectionNode.node.id === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The vault is filtered by the users individual vault
|
||||
*/
|
||||
get isMyVaultSelected(): boolean {
|
||||
return this.selectedOrganizationNode?.node.id === "MyVault";
|
||||
}
|
||||
|
||||
get isFavorites(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "favorites";
|
||||
}
|
||||
|
||||
get isDeleted(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "trash" ? true : null;
|
||||
}
|
||||
|
||||
get isArchived(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "archive";
|
||||
}
|
||||
|
||||
get organizationId(): string {
|
||||
return this.selectedOrganizationNode?.node.id;
|
||||
}
|
||||
|
||||
get cipherType(): CipherType {
|
||||
return isCipherType(this.selectedCipherTypeNode?.node.type)
|
||||
? this.selectedCipherTypeNode?.node.type
|
||||
: null;
|
||||
}
|
||||
|
||||
get cipherStatus(): CipherStatus {
|
||||
return this.selectedCipherTypeNode?.node.type;
|
||||
}
|
||||
|
||||
get cipherTypeId(): string {
|
||||
return this.selectedCipherTypeNode?.node.id;
|
||||
}
|
||||
|
||||
get folderId(): string {
|
||||
return this.selectedFolderNode?.node.id;
|
||||
}
|
||||
|
||||
get collectionId(): string {
|
||||
return this.selectedCollectionNode?.node.id;
|
||||
}
|
||||
|
||||
constructor(init?: Partial<VaultFilter>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
resetFilter() {
|
||||
this.selectedCipherTypeNode = null;
|
||||
this.selectedFolderNode = null;
|
||||
this.selectedCollectionNode = null;
|
||||
}
|
||||
|
||||
resetOrganization() {
|
||||
this.selectedOrganizationNode = null;
|
||||
}
|
||||
|
||||
buildFilter(): VaultFilterFunction {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.isFavorites && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.favorite;
|
||||
}
|
||||
if (this.isDeleted && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.isArchived && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isArchived;
|
||||
}
|
||||
if (this.cipherType && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.cipherType;
|
||||
}
|
||||
if (this.selectedFolderNode) {
|
||||
// No folder
|
||||
if (this.folderId === null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === null;
|
||||
}
|
||||
// Folder
|
||||
if (this.folderId !== null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === this.folderId;
|
||||
}
|
||||
}
|
||||
if (this.selectedCollectionNode) {
|
||||
// All Collections
|
||||
if (this.collectionId === "AllCollections" && cipherPassesFilter) {
|
||||
cipherPassesFilter = false;
|
||||
}
|
||||
// Unassigned
|
||||
if (this.collectionId === null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.organizationId != null &&
|
||||
(cipher.collectionIds == null || cipher.collectionIds.length === 0);
|
||||
}
|
||||
// Collection
|
||||
if (
|
||||
this.collectionId !== null &&
|
||||
this.collectionId !== "AllCollections" &&
|
||||
cipherPassesFilter
|
||||
) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null && cipher.collectionIds.includes(this.collectionId);
|
||||
}
|
||||
}
|
||||
if (this.selectedOrganizationNode) {
|
||||
// My Vault
|
||||
if (this.organizationId === "MyVault" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
// Organization
|
||||
else if (this.organizationId !== null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.organizationId;
|
||||
}
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
}
|
||||
22
libs/vault/src/models/vault-filter.type.ts
Normal file
22
libs/vault/src/models/vault-filter.type.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
export type CipherStatus = "all" | "favorites" | "archive" | "trash" | CipherType;
|
||||
|
||||
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
|
||||
export type CollectionFilter = CollectionAdminView & {
|
||||
icon: string;
|
||||
};
|
||||
export type FolderFilter = FolderView & {
|
||||
icon: string;
|
||||
/**
|
||||
* Full folder name.
|
||||
*
|
||||
* Used for when the folder `name` property is be separated into parts.
|
||||
*/
|
||||
fullName?: string;
|
||||
};
|
||||
export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean };
|
||||
@@ -2,11 +2,12 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of, Subject } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.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 { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
|
||||
189
libs/vault/src/services/routed-vault-filter-bridge.service.ts
Normal file
189
libs/vault/src/services/routed-vault-filter-bridge.service.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
|
||||
import { Unassigned } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import {
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
RoutedVaultFilterService,
|
||||
RoutedVaultFilterBridge,
|
||||
RoutedVaultFilterModel,
|
||||
All,
|
||||
VaultFilter,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
/**
|
||||
* This file is part of a layer that is used to temporary bridge between URL filtering and the old state-in-code method.
|
||||
* This should be removed after we have refactored the {@link VaultItemsComponent} and introduced vertical navigation
|
||||
* (which will refactor the {@link VaultFilterComponent}).
|
||||
*
|
||||
* This class listens to both the new {@link RoutedVaultFilterService} and the old {@link VaultFilterService}.
|
||||
* When a new filter is emitted the service uses the ids to find the corresponding tree nodes needed for
|
||||
* the old {@link VaultFilter} model. It then emits a bridge model that contains this information.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RoutedVaultFilterBridgeService {
|
||||
readonly activeFilter$: Observable<VaultFilter>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private routedVaultFilterService: RoutedVaultFilterService,
|
||||
legacyVaultFilterService: VaultFilterService,
|
||||
) {
|
||||
this.activeFilter$ = combineLatest([
|
||||
routedVaultFilterService.filter$,
|
||||
legacyVaultFilterService.collectionTree$,
|
||||
legacyVaultFilterService.folderTree$,
|
||||
legacyVaultFilterService.organizationTree$,
|
||||
legacyVaultFilterService.cipherTypeTree$,
|
||||
]).pipe(
|
||||
map(([filter, collectionTree, folderTree, organizationTree, cipherTypeTree]) => {
|
||||
const legacyFilter = isAdminConsole(filter)
|
||||
? createLegacyFilterForAdminConsole(filter, collectionTree, cipherTypeTree)
|
||||
: createLegacyFilterForEndUser(
|
||||
filter,
|
||||
collectionTree,
|
||||
folderTree,
|
||||
organizationTree,
|
||||
cipherTypeTree,
|
||||
);
|
||||
|
||||
return new RoutedVaultFilterBridge(filter, legacyFilter, this);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
navigate(filter: RoutedVaultFilterModel) {
|
||||
const [commands, extras] = this.routedVaultFilterService.createRoute(filter);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(commands, extras);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the filtering is being done as part of admin console.
|
||||
* Admin console can be identified by checking if the `organizationId`
|
||||
* is part of the path.
|
||||
*
|
||||
* @param filter Model to check if origin is admin console
|
||||
* @returns true if filtering being done as part of admin console
|
||||
*/
|
||||
function isAdminConsole(filter: RoutedVaultFilterModel) {
|
||||
return filter.organizationIdParamType === "path";
|
||||
}
|
||||
|
||||
function createLegacyFilterForAdminConsole(
|
||||
filter: RoutedVaultFilterModel,
|
||||
collectionTree: TreeNode<CollectionFilter>,
|
||||
cipherTypeTree: TreeNode<CipherTypeFilter>,
|
||||
): VaultFilter {
|
||||
const legacyFilter = new VaultFilter();
|
||||
|
||||
if (filter.collectionId === undefined && filter.type === undefined) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
"AllCollections",
|
||||
);
|
||||
} else if (filter.collectionId !== undefined && filter.collectionId === Unassigned) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(collectionTree, null);
|
||||
} else if (filter.collectionId !== undefined) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
filter.collectionId,
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.collectionId === undefined && filter.type === All) {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
"AllItems",
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type === "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(
|
||||
{ id: "trash", name: "", type: "trash", icon: "" },
|
||||
null,
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type !== "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
filter.type,
|
||||
);
|
||||
}
|
||||
|
||||
return legacyFilter;
|
||||
}
|
||||
|
||||
function createLegacyFilterForEndUser(
|
||||
filter: RoutedVaultFilterModel,
|
||||
collectionTree: TreeNode<CollectionFilter>,
|
||||
folderTree: TreeNode<FolderFilter>,
|
||||
organizationTree: TreeNode<OrganizationFilter>,
|
||||
cipherTypeTree: TreeNode<CipherTypeFilter>,
|
||||
): VaultFilter {
|
||||
const legacyFilter = new VaultFilter();
|
||||
|
||||
if (filter.collectionId !== undefined && filter.collectionId === Unassigned) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(collectionTree, null);
|
||||
} else if (filter.collectionId !== undefined && filter.collectionId === All) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
"AllCollections",
|
||||
);
|
||||
} else if (filter.collectionId !== undefined) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
filter.collectionId,
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.folderId !== undefined && filter.folderId === Unassigned) {
|
||||
legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, null);
|
||||
} else if (filter.folderId !== undefined && filter.folderId !== Unassigned) {
|
||||
legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, filter.folderId);
|
||||
}
|
||||
|
||||
if (filter.organizationId !== undefined && filter.organizationId === Unassigned) {
|
||||
legacyFilter.selectedOrganizationNode = ServiceUtils.getTreeNodeObject(
|
||||
organizationTree,
|
||||
"MyVault",
|
||||
);
|
||||
} else if (filter.organizationId !== undefined && filter.organizationId !== Unassigned) {
|
||||
legacyFilter.selectedOrganizationNode = ServiceUtils.getTreeNodeObject(
|
||||
organizationTree,
|
||||
filter.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.type === undefined) {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
"AllItems",
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type === "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(
|
||||
{ id: "trash", name: "", type: "trash", icon: "" },
|
||||
null,
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type === "archive") {
|
||||
legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(
|
||||
{ id: "archive", name: "", type: "archive", icon: "" },
|
||||
null,
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type !== "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
filter.type,
|
||||
);
|
||||
}
|
||||
|
||||
return legacyFilter;
|
||||
}
|
||||
95
libs/vault/src/services/routed-vault-filter.service.ts
Normal file
95
libs/vault/src/services/routed-vault-filter.service.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Injectable, OnDestroy, inject } from "@angular/core";
|
||||
import { ActivatedRoute, NavigationExtras } from "@angular/router";
|
||||
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
isRoutedVaultFilterItemType,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../models/routed-vault-filter.model";
|
||||
|
||||
/**
|
||||
* Injection token for the base route path used in vault filter navigation.
|
||||
*/
|
||||
export const VAULT_FILTER_BASE_ROUTE = new SafeInjectionToken<string>("VaultFilterBaseRoute");
|
||||
|
||||
/**
|
||||
* This service is an abstraction layer on top of ActivatedRoute that
|
||||
* encapsulates the logic of how filters are stored in the URL.
|
||||
*
|
||||
* The service builds and emits filter models based on URL params and
|
||||
* also contains a method for generating routes to corresponding to those params.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RoutedVaultFilterService implements OnDestroy {
|
||||
private onDestroy = new Subject<void>();
|
||||
private baseRoute: string = inject(VAULT_FILTER_BASE_ROUTE, { optional: true }) ?? "";
|
||||
|
||||
/**
|
||||
* Filter values extracted from the URL.
|
||||
* To change the values use {@link RoutedVaultFilterService.createRoute}.
|
||||
*/
|
||||
filter$: Observable<RoutedVaultFilterModel>;
|
||||
|
||||
constructor(activatedRoute: ActivatedRoute) {
|
||||
this.filter$ = combineLatest([activatedRoute.paramMap, activatedRoute.queryParamMap]).pipe(
|
||||
map(([params, queryParams]) => {
|
||||
const unsafeType = queryParams.get("type");
|
||||
const type = isRoutedVaultFilterItemType(unsafeType) ? unsafeType : undefined;
|
||||
|
||||
return {
|
||||
collectionId: (queryParams.get("collectionId") as CollectionId) ?? undefined,
|
||||
folderId: queryParams.get("folderId") ?? undefined,
|
||||
organizationId:
|
||||
(params.get("organizationId") as OrganizationId) ??
|
||||
(queryParams.get("organizationId") as OrganizationId) ??
|
||||
undefined,
|
||||
organizationIdParamType:
|
||||
params.get("organizationId") != undefined ? ("path" as const) : ("query" as const),
|
||||
type,
|
||||
};
|
||||
}),
|
||||
takeUntil(this.onDestroy),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a route that can be used to modify filters with Router or RouterLink.
|
||||
* This method is specifically built to leave other query parameters untouched,
|
||||
* meaning that navigation will only affect filters and not e.g. `cipherId`.
|
||||
* To subscribe to changes use {@link RoutedVaultFilterService.filter$}.
|
||||
*
|
||||
* Note:
|
||||
* This method currently only supports changing filters that are stored
|
||||
* in query parameters. This means that {@link RoutedVaultFilterModel.organizationId}
|
||||
* will be ignored if {@link RoutedVaultFilterModel.organizationIdParamType}
|
||||
* is set to `path`.
|
||||
*
|
||||
* @param filter Filter values that should be applied to the URL.
|
||||
* @returns route that can be used with Router or RouterLink
|
||||
*/
|
||||
createRoute(filter: RoutedVaultFilterModel): [commands: any[], extras?: NavigationExtras] {
|
||||
const commands: string[] = this.baseRoute ? [this.baseRoute] : [];
|
||||
const extras: NavigationExtras = {
|
||||
queryParams: {
|
||||
collectionId: filter.collectionId ?? null,
|
||||
folderId: filter.folderId ?? null,
|
||||
organizationId:
|
||||
filter.organizationIdParamType === "path" ? null : (filter.organizationId ?? null),
|
||||
type: filter.type ?? null,
|
||||
},
|
||||
queryParamsHandling: "merge",
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
};
|
||||
return [commands, extras];
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
}
|
||||
405
libs/vault/src/services/vault-filter.service.spec.ts
Normal file
405
libs/vault/src/services/vault-filter.service.spec.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
import {
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/../spec/fake-account-service";
|
||||
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, of, ReplaySubject } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } 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 {
|
||||
CollectionView,
|
||||
CollectionType,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
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 { CollectionId, OrganizationId, 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
|
||||
jest.mock("@bitwarden/angular/vault/vault-filter/services/vault-filter.service", () => ({
|
||||
sortDefaultCollections: jest.fn((): CollectionView[] => []),
|
||||
}));
|
||||
|
||||
describe("vault filter service", () => {
|
||||
let vaultFilterService: VaultFilterService;
|
||||
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let folderService: MockProxy<FolderService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let collectionService: MockProxy<CollectionService>;
|
||||
let organizations: ReplaySubject<Organization[]>;
|
||||
let folderViews: ReplaySubject<FolderView[]>;
|
||||
let collectionViews: ReplaySubject<CollectionView[]>;
|
||||
let cipherViews: ReplaySubject<CipherView[]>;
|
||||
let organizationDataOwnershipPolicy: ReplaySubject<boolean>;
|
||||
let singleOrgPolicy: ReplaySubject<boolean>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
let collapsedGroupingsState: FakeSingleUserState<string[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
organizationService = mock<OrganizationService>();
|
||||
folderService = mock<FolderService>();
|
||||
cipherService = mock<CipherService>();
|
||||
policyService = mock<PolicyService>();
|
||||
i18nService = mock<I18nService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
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);
|
||||
collectionViews = new ReplaySubject<CollectionView[]>(1);
|
||||
cipherViews = new ReplaySubject<CipherView[]>(1);
|
||||
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$.mockReturnValue(collectionViews);
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.OrganizationDataOwnership, mockUserId)
|
||||
.mockReturnValue(organizationDataOwnershipPolicy);
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.SingleOrg, mockUserId)
|
||||
.mockReturnValue(singleOrgPolicy);
|
||||
cipherService.cipherListViews$.mockReturnValue(cipherViews);
|
||||
|
||||
vaultFilterService = new VaultFilterService(
|
||||
organizationService,
|
||||
folderService,
|
||||
cipherService,
|
||||
policyService,
|
||||
i18nService,
|
||||
stateProvider,
|
||||
collectionService,
|
||||
accountService,
|
||||
);
|
||||
collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS);
|
||||
organizations.next([]);
|
||||
});
|
||||
|
||||
describe("collapsed filter nodes", () => {
|
||||
const nodes = new Set(["1", "2"]);
|
||||
|
||||
it("should update the collapsedFilterNodes$", async () => {
|
||||
await vaultFilterService.setCollapsedFilterNodes(nodes, mockUserId);
|
||||
|
||||
const collapsedGroupingsState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
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 () => {
|
||||
collapsedGroupingsState.nextState(["1", "2"]);
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.collapsedFilterNodes$)).resolves.toEqual(
|
||||
nodes,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("organizations", () => {
|
||||
beforeEach(() => {
|
||||
const storedOrgs = [
|
||||
createOrganization("1" as OrganizationId, "org1"),
|
||||
createOrganization("2" as OrganizationId, "org2"),
|
||||
];
|
||||
organizations.next(storedOrgs);
|
||||
organizationDataOwnershipPolicy.next(false);
|
||||
singleOrgPolicy.next(false);
|
||||
});
|
||||
|
||||
it("returns a nested tree", async () => {
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(3);
|
||||
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||
expect(tree.children.find((o) => o.node.name === "org2"));
|
||||
});
|
||||
|
||||
it("hides My Vault if organization data ownership policy is enabled", async () => {
|
||||
organizationDataOwnershipPolicy.next(true);
|
||||
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(2);
|
||||
expect(!tree.children.find((o) => o.node.id === "MyVault"));
|
||||
});
|
||||
|
||||
it("returns 1 organization and My Vault if single organization policy is enabled", async () => {
|
||||
singleOrgPolicy.next(true);
|
||||
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(2);
|
||||
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||
expect(tree.children.find((o) => o.node.id === "MyVault"));
|
||||
});
|
||||
|
||||
it("returns 1 organization if both single organization and organization data ownership policies are enabled", async () => {
|
||||
singleOrgPolicy.next(true);
|
||||
organizationDataOwnershipPolicy.next(true);
|
||||
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(1);
|
||||
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("folders", () => {
|
||||
describe("filtered folders with organization", () => {
|
||||
beforeEach(() => {
|
||||
// Org must be updated before folderService else the subscription uses the null org default value
|
||||
vaultFilterService.setOrganizationFilter(
|
||||
createOrganization("org test id" as OrganizationId, "Test Org"),
|
||||
);
|
||||
});
|
||||
it("returns folders filtered by current organization", async () => {
|
||||
const storedCiphers = [
|
||||
createCipherView("1", "org test id", "folder test id"),
|
||||
createCipherView("2", "non matching org id", "non matching folder id"),
|
||||
];
|
||||
cipherViews.next(storedCiphers);
|
||||
|
||||
const storedFolders = [
|
||||
createFolderView("folder test id", "test"),
|
||||
createFolderView("non matching folder id", "test2"),
|
||||
];
|
||||
folderViews.next(storedFolders);
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.filteredFolders$)).resolves.toEqual([
|
||||
createFolderView("folder test id", "test"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns current organization", () => {
|
||||
vaultFilterService.getOrganizationFilter().subscribe((org) => {
|
||||
expect(org.id).toEqual("org test id");
|
||||
expect(org.identifier).toEqual("Test Org");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("folder tree", () => {
|
||||
it("returns a nested tree", async () => {
|
||||
const storedFolders = [
|
||||
createFolderView("Folder 1 Id", "Folder 1"),
|
||||
createFolderView("Folder 2 Id", "Folder 1/Folder 2"),
|
||||
createFolderView("Folder 3 Id", "Folder 1/Folder 3"),
|
||||
];
|
||||
folderViews.next(storedFolders);
|
||||
cipherViews.next([]);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.folderTree$);
|
||||
|
||||
expect(result.children[0].node.id === "Folder 1 Id");
|
||||
expect(result.children[0].children.find((c) => c.node.id === "Folder 2 Id"));
|
||||
expect(result.children[0].children.find((c) => c.node.id === "Folder 3 Id"));
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collections", () => {
|
||||
describe("filtered collections", () => {
|
||||
it("returns collections filtered by current organization", async () => {
|
||||
vaultFilterService.setOrganizationFilter(
|
||||
createOrganization("org test id" as OrganizationId, "Test Org"),
|
||||
);
|
||||
|
||||
const storedCollections = [
|
||||
createCollectionView("1", "collection 1", "org test id"),
|
||||
createCollectionView("2", "collection 2", "non matching org id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.filteredCollections$)).resolves.toEqual([
|
||||
createCollectionView("1", "collection 1", "org test id"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collection tree", () => {
|
||||
it("returns tree with children", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 3", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
|
||||
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-2", "id-3"]);
|
||||
});
|
||||
|
||||
it("returns tree where non-existing collections are excluded from children", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
|
||||
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-3"]);
|
||||
expect(result.children[0].children[0].node.name).toBe("Collection 2/Collection 3");
|
||||
});
|
||||
|
||||
it("returns tree with parents", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
createCollectionView("id-4", "Collection 1/Collection 4", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
const c1 = result.children[0];
|
||||
const c2 = c1.children[0];
|
||||
const c3 = c2.children[0];
|
||||
const c4 = c1.children[1];
|
||||
expect(c2.parent.node.id).toEqual("id-1");
|
||||
expect(c3.parent.node.id).toEqual("id-2");
|
||||
expect(c4.parent.node.id).toEqual("id-1");
|
||||
});
|
||||
|
||||
it("returns tree where non-existing collections are excluded from parents", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
const c1 = result.children[0];
|
||||
const c3 = c1.children[0];
|
||||
expect(c3.parent.node.id).toEqual("id-1");
|
||||
});
|
||||
|
||||
it("calls sortDefaultCollections with the correct args", async () => {
|
||||
const storedOrgs = [
|
||||
createOrganization("id-defaultOrg1" as OrganizationId, "org1"),
|
||||
createOrganization("id-defaultOrg2" as OrganizationId, "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);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(vaultFilterSvc.sortDefaultCollections).toHaveBeenCalledWith(
|
||||
storedCollections,
|
||||
storedOrgs,
|
||||
i18nService.collator,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createOrganization(id: OrganizationId, name: string) {
|
||||
const org = new Organization();
|
||||
org.id = id;
|
||||
org.name = name;
|
||||
org.identifier = name;
|
||||
org.isMember = true;
|
||||
return org;
|
||||
}
|
||||
|
||||
function createCipherView(id: string, orgId: string, folderId: string) {
|
||||
const cipher = new CipherView();
|
||||
cipher.id = id;
|
||||
cipher.organizationId = orgId;
|
||||
cipher.folderId = folderId;
|
||||
return cipher;
|
||||
}
|
||||
|
||||
function createFolderView(id: string, name: string): FolderView {
|
||||
const folder = new FolderView();
|
||||
folder.id = id;
|
||||
folder.name = name;
|
||||
return folder;
|
||||
}
|
||||
|
||||
function createCollectionView(
|
||||
id: string,
|
||||
name: string,
|
||||
orgId: string,
|
||||
type?: CollectionType,
|
||||
): CollectionView {
|
||||
const collection = new CollectionView({
|
||||
id: id as CollectionId,
|
||||
name,
|
||||
organizationId: orgId as OrganizationId,
|
||||
});
|
||||
|
||||
if (type) {
|
||||
collection.type = type;
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
});
|
||||
363
libs/vault/src/services/vault-filter.service.ts
Normal file
363
libs/vault/src/services/vault-filter.service.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } 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 {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { cloneCollection } from "@bitwarden/common/admin-console/utils/collection-utils";
|
||||
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 { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId, 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";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
import {
|
||||
VaultFilterServiceAbstraction,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
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(
|
||||
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)),
|
||||
),
|
||||
this.activeUserId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
||||
),
|
||||
),
|
||||
]).pipe(
|
||||
switchMap(([orgs, singleOrgPolicy, organizationDataOwnershipPolicy]) =>
|
||||
this.buildOrganizationTree(orgs, singleOrgPolicy, organizationDataOwnershipPolicy),
|
||||
),
|
||||
);
|
||||
|
||||
protected _organizationFilter = new BehaviorSubject<Organization>(null);
|
||||
|
||||
filteredFolders$: Observable<FolderView[]> = this.activeUserId$.pipe(
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.folderService.folderViews$(userId),
|
||||
this.cipherService.cipherListViews$(userId),
|
||||
this._organizationFilter,
|
||||
]),
|
||||
),
|
||||
filter(([folders, ciphers, org]) => !!ciphers), // ciphers may be null, meaning decryption is in progress. Ignore this emission
|
||||
switchMap(([folders, ciphers, org]) => {
|
||||
return this.filterFolders(folders, ciphers, org);
|
||||
}),
|
||||
);
|
||||
|
||||
folderTree$: Observable<TreeNode<FolderFilter>> = this.filteredFolders$.pipe(
|
||||
map((folders) => this.buildFolderTree(folders)),
|
||||
);
|
||||
|
||||
filteredCollections$: Observable<CollectionView[]> = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
),
|
||||
this._organizationFilter,
|
||||
]).pipe(switchMap(([collections, org]) => this.filterCollections(collections, org)));
|
||||
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>> = combineLatest([
|
||||
this.filteredCollections$,
|
||||
this.memberOrganizations$,
|
||||
]).pipe(
|
||||
map(([collections, organizations]) => this.buildCollectionTree(collections, organizations)),
|
||||
);
|
||||
|
||||
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree();
|
||||
|
||||
private collapsedGroupingsState(userId: UserId): SingleUserState<string[]> {
|
||||
return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected organizationService: OrganizationService,
|
||||
protected folderService: FolderService,
|
||||
protected cipherService: CipherService,
|
||||
protected policyService: PolicyService,
|
||||
protected i18nService: I18nService,
|
||||
protected stateProvider: StateProvider,
|
||||
protected collectionService: CollectionService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async getCollectionNodeFromTree(id: string) {
|
||||
const collections = await firstValueFrom(this.collectionTree$);
|
||||
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionFilter>;
|
||||
}
|
||||
|
||||
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>, userId: UserId): Promise<void> {
|
||||
await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes));
|
||||
}
|
||||
|
||||
protected async getCollapsedFilterNodes(): Promise<Set<string>> {
|
||||
return await firstValueFrom(this.collapsedFilterNodes$);
|
||||
}
|
||||
|
||||
getOrganizationFilter() {
|
||||
return this._organizationFilter;
|
||||
}
|
||||
|
||||
clearOrganizationFilter() {
|
||||
this._organizationFilter.next(null);
|
||||
}
|
||||
|
||||
setOrganizationFilter(organization: Organization) {
|
||||
if (organization?.id != "AllVaults") {
|
||||
this._organizationFilter.next(organization);
|
||||
} else {
|
||||
this._organizationFilter.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
async expandOrgFilter(userId: UserId) {
|
||||
const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$);
|
||||
if (!collapsedFilterNodes.has("AllVaults")) {
|
||||
return;
|
||||
}
|
||||
collapsedFilterNodes.delete("AllVaults");
|
||||
await this.setCollapsedFilterNodes(collapsedFilterNodes, userId);
|
||||
}
|
||||
|
||||
protected async buildOrganizationTree(
|
||||
orgs: Organization[],
|
||||
singleOrgPolicy: boolean,
|
||||
organizationDataOwnershipPolicy: boolean,
|
||||
): Promise<TreeNode<OrganizationFilter>> {
|
||||
const headNode = this.getOrganizationFilterHead();
|
||||
if (!organizationDataOwnershipPolicy) {
|
||||
const myVaultNode = this.getOrganizationFilterMyVault();
|
||||
headNode.children.push(myVaultNode);
|
||||
}
|
||||
if (singleOrgPolicy) {
|
||||
orgs = orgs.slice(0, 1);
|
||||
}
|
||||
if (orgs) {
|
||||
const orgNodes: TreeNode<OrganizationFilter>[] = [];
|
||||
orgs.forEach((org) => {
|
||||
const orgCopy = org as OrganizationFilter;
|
||||
orgCopy.icon = "bwi-business";
|
||||
const node = new TreeNode<OrganizationFilter>(orgCopy, headNode, orgCopy.name);
|
||||
orgNodes.push(node);
|
||||
});
|
||||
// Sort organization nodes, then add them to the list after 'My Vault' and 'All Vaults' if present
|
||||
orgNodes.sort((a, b) => a.node.name.localeCompare(b.node.name));
|
||||
headNode.children.push(...orgNodes);
|
||||
}
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getOrganizationFilterHead(): TreeNode<OrganizationFilter> {
|
||||
const head = new Organization() as OrganizationFilter;
|
||||
head.enabled = true;
|
||||
return new TreeNode<OrganizationFilter>(head, null, "allVaults", "AllVaults");
|
||||
}
|
||||
|
||||
protected getOrganizationFilterMyVault(): TreeNode<OrganizationFilter> {
|
||||
const myVault = new Organization() as OrganizationFilter;
|
||||
myVault.id = "MyVault" as OrganizationId;
|
||||
myVault.icon = "bwi-user";
|
||||
myVault.enabled = true;
|
||||
myVault.hideOptions = true;
|
||||
return new TreeNode<OrganizationFilter>(myVault, null, this.i18nService.t("myVault"));
|
||||
}
|
||||
|
||||
buildTypeTree(
|
||||
head: CipherTypeFilter,
|
||||
array?: CipherTypeFilter[],
|
||||
): Observable<TreeNode<CipherTypeFilter>> {
|
||||
const headNode = new TreeNode<CipherTypeFilter>(head, null);
|
||||
array?.forEach((filter) => {
|
||||
const node = new TreeNode<CipherTypeFilter>(filter, headNode, filter.name);
|
||||
headNode.children.push(node);
|
||||
});
|
||||
return of(headNode);
|
||||
}
|
||||
|
||||
protected async filterCollections(
|
||||
storedCollections: CollectionView[],
|
||||
org?: Organization,
|
||||
): Promise<CollectionView[]> {
|
||||
return org?.id != null
|
||||
? storedCollections.filter((c) => c.organizationId === org.id)
|
||||
: storedCollections;
|
||||
}
|
||||
|
||||
protected buildCollectionTree(
|
||||
collections?: CollectionView[],
|
||||
orgs?: Organization[],
|
||||
): TreeNode<CollectionFilter> {
|
||||
const headNode = this.getCollectionFilterHead();
|
||||
if (!collections) {
|
||||
return headNode;
|
||||
}
|
||||
const all: TreeNode<CollectionFilter>[] = [];
|
||||
collections = sortDefaultCollections(collections, orgs, this.i18nService.collator);
|
||||
const groupedByOrg = this.collectionService.groupByOrganization(collections);
|
||||
|
||||
for (const group of groupedByOrg.values()) {
|
||||
const nodes: TreeNode<CollectionFilter>[] = [];
|
||||
for (const c of group) {
|
||||
const collectionCopy = cloneCollection(
|
||||
new CollectionView({ ...c, name: c.name }),
|
||||
) as CollectionFilter;
|
||||
collectionCopy.icon =
|
||||
c.type === CollectionTypes.DefaultUserCollection ? "bwi-user" : "bwi-collection-shared";
|
||||
const parts = c.name ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter);
|
||||
}
|
||||
all.push(...nodes);
|
||||
}
|
||||
|
||||
all.forEach((n) => {
|
||||
n.parent = headNode;
|
||||
headNode.children.push(n);
|
||||
});
|
||||
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getCollectionFilterHead(): TreeNode<CollectionFilter> {
|
||||
const head = CollectionView.vaultFilterHead() as CollectionFilter;
|
||||
return new TreeNode<CollectionFilter>(head, null, "collections", "AllCollections");
|
||||
}
|
||||
|
||||
protected async filterFolders(
|
||||
storedFolders: FolderView[],
|
||||
ciphers: CipherView[] | CipherListView[],
|
||||
org?: Organization,
|
||||
): Promise<FolderView[]> {
|
||||
// If no org or "My Vault" is selected, show all folders
|
||||
if (org?.id == null || org?.id == "MyVault") {
|
||||
return storedFolders;
|
||||
}
|
||||
|
||||
// Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder
|
||||
const orgCiphers = ciphers.filter((c) => c.organizationId == org?.id);
|
||||
return storedFolders.filter(
|
||||
(f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null,
|
||||
);
|
||||
}
|
||||
|
||||
protected buildFolderTree(folders?: FolderView[]): TreeNode<FolderFilter> {
|
||||
const headNode = this.getFolderFilterHead();
|
||||
if (!folders) {
|
||||
return headNode;
|
||||
}
|
||||
const nodes: TreeNode<FolderFilter>[] = [];
|
||||
folders.forEach((f) => {
|
||||
const folderCopy = new FolderView() as FolderFilter;
|
||||
folderCopy.id = f.id;
|
||||
folderCopy.revisionDate = f.revisionDate;
|
||||
folderCopy.icon = "bwi-folder";
|
||||
folderCopy.fullName = f.name; // save full folder name before separating it into parts
|
||||
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
|
||||
});
|
||||
|
||||
nodes.forEach((n) => {
|
||||
n.parent = headNode;
|
||||
headNode.children.push(n);
|
||||
});
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getFolderFilterHead(): TreeNode<FolderFilter> {
|
||||
const head = new FolderView() as FolderFilter;
|
||||
return new TreeNode<FolderFilter>(head, null, "folders", "AllFolders");
|
||||
}
|
||||
|
||||
protected buildCipherTypeTree(): Observable<TreeNode<CipherTypeFilter>> {
|
||||
const allTypeFilters: CipherTypeFilter[] = [
|
||||
{
|
||||
id: "favorites",
|
||||
name: this.i18nService.t("favorites"),
|
||||
type: "favorites",
|
||||
icon: "bwi-star",
|
||||
},
|
||||
{
|
||||
id: "login",
|
||||
name: this.i18nService.t("typeLogin"),
|
||||
type: CipherType.Login,
|
||||
icon: "bwi-globe",
|
||||
},
|
||||
{
|
||||
id: "card",
|
||||
name: this.i18nService.t("typeCard"),
|
||||
type: CipherType.Card,
|
||||
icon: "bwi-credit-card",
|
||||
},
|
||||
{
|
||||
id: "identity",
|
||||
name: this.i18nService.t("typeIdentity"),
|
||||
type: CipherType.Identity,
|
||||
icon: "bwi-id-card",
|
||||
},
|
||||
{
|
||||
id: "note",
|
||||
name: this.i18nService.t("typeSecureNote"),
|
||||
type: CipherType.SecureNote,
|
||||
icon: "bwi-sticky-note",
|
||||
},
|
||||
{
|
||||
id: "sshKey",
|
||||
name: this.i18nService.t("typeSshKey"),
|
||||
type: CipherType.SshKey,
|
||||
icon: "bwi-key",
|
||||
},
|
||||
];
|
||||
|
||||
return this.buildTypeTree(
|
||||
{ id: "AllItems", name: "allItems", type: "all", icon: "" },
|
||||
allTypeFilters,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user