From 1dc9e5ff9acbf4869f88c53620cbbfe218318110 Mon Sep 17 00:00:00 2001 From: addison Date: Thu, 24 Mar 2022 11:51:51 -0400 Subject: [PATCH] [refactor] Break down vault filter into several components These changes rename and rewrite the GroupingsComponent into a VaultFiltersModule. The module follows typical angular patterns for structure and purpose, and contain components for each filter type. The mostly communicate via Input and Output, and depend on a VaultFilterService for sending and recieving data from other parts of the product. --- angular/src/components/groupings.component.ts | 253 ------------------ .../components/collection-filter.component.ts | 51 ++++ .../components/folder-filter.component.ts | 58 ++++ .../organization-filter.component.ts | 84 ++++++ .../components/status-filter.component.ts | 22 ++ .../components/type-filter.component.ts | 40 +++ .../models/cipher-status.model.ts | 1 + .../models/dynamic-tree-node.model.ts | 16 ++ .../models/top-level-tree-node.model.ts | 7 + .../vault-filter/models/vault-filter.model.ts | 32 +++ .../vault-filter/vault-filter.component.ts | 95 +++++++ .../vault-filter/vault-filter.service.ts | 79 ++++++ 12 files changed, 485 insertions(+), 253 deletions(-) delete mode 100644 angular/src/components/groupings.component.ts create mode 100644 angular/src/modules/vault-filter/components/collection-filter.component.ts create mode 100644 angular/src/modules/vault-filter/components/folder-filter.component.ts create mode 100644 angular/src/modules/vault-filter/components/organization-filter.component.ts create mode 100644 angular/src/modules/vault-filter/components/status-filter.component.ts create mode 100644 angular/src/modules/vault-filter/components/type-filter.component.ts create mode 100644 angular/src/modules/vault-filter/models/cipher-status.model.ts create mode 100644 angular/src/modules/vault-filter/models/dynamic-tree-node.model.ts create mode 100644 angular/src/modules/vault-filter/models/top-level-tree-node.model.ts create mode 100644 angular/src/modules/vault-filter/models/vault-filter.model.ts create mode 100644 angular/src/modules/vault-filter/vault-filter.component.ts create mode 100644 angular/src/modules/vault-filter/vault-filter.service.ts diff --git a/angular/src/components/groupings.component.ts b/angular/src/components/groupings.component.ts deleted file mode 100644 index 670862e7..00000000 --- a/angular/src/components/groupings.component.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { Directive, EventEmitter, Input, Output } from "@angular/core"; - -import { CipherService } from "jslib-common/abstractions/cipher.service"; -import { CollectionService } from "jslib-common/abstractions/collection.service"; -import { FolderService } from "jslib-common/abstractions/folder.service"; -import { OrganizationService } from "jslib-common/abstractions/organization.service"; -import { StateService } from "jslib-common/abstractions/state.service"; -import { CipherType } from "jslib-common/enums/cipherType"; -import { Organization } from "jslib-common/models/domain/organization"; -import { ITreeNodeObject, TreeNode } from "jslib-common/models/domain/treeNode"; -import { CollectionView } from "jslib-common/models/view/collectionView"; -import { FolderView } from "jslib-common/models/view/folderView"; - -export type TopLevelGroupingId = "vaults" | "types" | "collections" | "folders"; -export class TopLevelGroupingView implements ITreeNodeObject { - id: TopLevelGroupingId; - name: string; // localizationString -} -@Directive() -export class GroupingsComponent { - @Input() showFolders = true; - @Input() showCollections = true; - @Input() showFavorites = true; - @Input() showTrash = true; - @Input() showOrganizations = true; - - @Output() onAllClicked = new EventEmitter(); - @Output() onFavoritesClicked = new EventEmitter(); - @Output() onTrashClicked = new EventEmitter(); - @Output() onCipherTypeClicked = new EventEmitter(); - @Output() onFolderClicked = new EventEmitter(); - @Output() onAddFolder = new EventEmitter(); - @Output() onEditFolder = new EventEmitter(); - @Output() onCollectionClicked = new EventEmitter(); - @Output() onOrganizationClicked = new EventEmitter(); - @Output() onMyVaultClicked = new EventEmitter(); - @Output() onAllVaultsClicked = new EventEmitter(); - - folders: FolderView[]; - nestedFolders: TreeNode[]; - collections: CollectionView[]; - nestedCollections: TreeNode[]; - loaded = false; - cipherType = CipherType; - selectedAll = false; - selectedFavorites = false; - selectedTrash = false; - selectedType: CipherType = null; - selectedFolder = false; - selectedFolderId: string = null; - selectedCollectionId: string = null; - selectedOrganizationId: string = null; - organizations: Organization[]; - myVaultOnly = false; - - readonly vaultsGrouping: TopLevelGroupingView = { - id: "vaults", - name: "allVaults", - }; - - readonly typesGrouping: TopLevelGroupingView = { - id: "types", - name: "types", - }; - - readonly collectionsGrouping: TopLevelGroupingView = { - id: "collections", - name: "collections", - }; - - readonly foldersGrouping: TopLevelGroupingView = { - id: "folders", - name: "folders", - }; - - private collapsedGroupings: Set; - - constructor( - protected collectionService: CollectionService, - protected folderService: FolderService, - protected stateService: StateService, - protected organizationService: OrganizationService, - protected cipherService: CipherService - ) {} - - async load(setLoaded = true) { - const collapsedGroupings = await this.stateService.getCollapsedGroupings(); - if (collapsedGroupings == null) { - this.collapsedGroupings = new Set(); - } else { - this.collapsedGroupings = new Set(collapsedGroupings); - } - - await this.loadFolders(); - await this.loadCollections(); - await this.loadOrganizations(); - - if (setLoaded) { - this.loaded = true; - } - } - - async loadCollections(organizationId?: string) { - if (!this.showCollections) { - return; - } - const collections = await this.collectionService.getAllDecrypted(); - if (organizationId != null) { - this.collections = collections.filter((c) => c.organizationId === organizationId); - } else { - this.collections = collections; - } - this.nestedCollections = await this.collectionService.getAllNested(this.collections); - } - - async loadFolders(organizationId?: string) { - if (!this.showFolders) { - return; - } - const folders = await this.folderService.getAllDecrypted(); - if (organizationId != null) { - const ciphers = await this.cipherService.getAllDecrypted(); - const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId); - this.folders = folders.filter( - (f) => - f.id != null && - (orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 || - ciphers.filter((c) => c.folderId == f.id).length < 1) - ); - } else { - this.folders = folders; - } - this.nestedFolders = await this.folderService.getAllNested(this.folders); - } - - async loadOrganizations() { - this.showOrganizations = await this.organizationService.hasOrganizations(); - if (!this.showOrganizations) { - return; - } - this.organizations = await this.organizationService.getAll(); - } - - selectAll() { - this.clearSelections(); - this.selectedAll = true; - this.onAllClicked.emit(); - } - - selectFavorites() { - this.clearSelections(); - this.selectedFavorites = true; - this.onFavoritesClicked.emit(); - } - - selectTrash() { - this.clearSelections(); - this.selectedTrash = true; - this.onTrashClicked.emit(); - } - - selectType(type: CipherType) { - this.clearSelections(); - this.selectedType = type; - this.onCipherTypeClicked.emit(type); - } - - selectFolder(folder: FolderView) { - this.clearSelections(); - this.selectedFolder = true; - this.selectedFolderId = folder.id; - this.onFolderClicked.emit(folder); - } - - addFolder() { - this.onAddFolder.emit(); - } - - editFolder(folder: FolderView) { - this.onEditFolder.emit(folder); - } - - selectCollection(collection: CollectionView) { - this.clearSelections(); - this.selectedCollectionId = collection.id; - this.onCollectionClicked.emit(collection); - } - - async selectOrganization(organization: Organization) { - this.clearSelectedOrganization(); - this.selectedOrganizationId = organization.id; - await this.reloadCollectionsAndFolders(this.selectedOrganizationId); - this.onOrganizationClicked.emit(organization); - } - - async selectMyVault() { - this.clearSelectedOrganization(); - this.myVaultOnly = true; - await this.reloadCollectionsAndFolders(this.selectedOrganizationId); - this.onMyVaultClicked.emit(); - } - - async selectAllVaults() { - this.clearSelectedOrganization(); - await this.reloadCollectionsAndFolders(this.selectedOrganizationId); - this.onAllVaultsClicked.emit(); - } - - clearSelections() { - this.selectedAll = false; - this.selectedFavorites = false; - this.selectedTrash = false; - this.selectedType = null; - this.selectedFolder = false; - this.selectedFolderId = null; - this.selectedCollectionId = null; - } - - clearSelectedOrganization() { - this.selectedOrganizationId = null; - this.myVaultOnly = false; - const clearingFolderOrCollectionSelection = - this.selectedFolderId != null || this.selectedCollectionId != null; - if (clearingFolderOrCollectionSelection) { - this.selectedFolder = false; - this.selectedFolderId = null; - this.selectedCollectionId = null; - this.selectedAll = true; - } - } - - async collapse(node: ITreeNodeObject, idPrefix = "") { - if (node.id == null) { - return; - } - const id = idPrefix + node.id; - if (this.isCollapsed(node, idPrefix)) { - this.collapsedGroupings.delete(id); - } else { - this.collapsedGroupings.add(id); - } - await this.stateService.setCollapsedGroupings(Array.from(this.collapsedGroupings)); - } - - isCollapsed(node: ITreeNodeObject, idPrefix = "") { - return this.collapsedGroupings.has(idPrefix + node.id); - } - - private async reloadCollectionsAndFolders(organizationId?: string) { - await this.loadCollections(organizationId); - await this.loadFolders(organizationId); - } -} diff --git a/angular/src/modules/vault-filter/components/collection-filter.component.ts b/angular/src/modules/vault-filter/components/collection-filter.component.ts new file mode 100644 index 00000000..67a21200 --- /dev/null +++ b/angular/src/modules/vault-filter/components/collection-filter.component.ts @@ -0,0 +1,51 @@ +import { Directive, EventEmitter, Input, Output } from "@angular/core"; + +import { ITreeNodeObject } from "jslib-common/models/domain/treeNode"; +import { CollectionView } from "jslib-common/models/view/collectionView"; + +import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; +import { TopLevelTreeNode } from "../models/top-level-tree-node.model"; +import { VaultFilter } from "../models/vault-filter.model"; + +@Directive() +export class CollectionFilterComponent { + @Input() hide = false; + @Input() collapsedFilterNodes: Set; + @Input() collectionNodes: DynamicTreeNode; + @Input() activeFilter: VaultFilter; + + @Output() onNodeCollapseStateChange: EventEmitter = + new EventEmitter(); + @Output() onFilterChange: EventEmitter = new EventEmitter(); + + readonly collectionsGrouping: TopLevelTreeNode = { + id: "collections", + name: "collections", + }; + + get collections() { + return this.collectionNodes?.fullList; + } + + get nestedCollections() { + return this.collectionNodes?.nestedList; + } + + get show() { + return !this.hide && this.collections != null && this.collections.length > 0; + } + + isCollapsed(node: ITreeNodeObject) { + return this.collapsedFilterNodes.has(node.id); + } + + applyFilter(collection: CollectionView) { + this.activeFilter.resetFilter(); + this.activeFilter.selectedCollectionId = collection.id; + this.onFilterChange.emit(this.activeFilter); + } + + async toggleCollapse(node: ITreeNodeObject) { + this.onNodeCollapseStateChange.emit(node); + } +} diff --git a/angular/src/modules/vault-filter/components/folder-filter.component.ts b/angular/src/modules/vault-filter/components/folder-filter.component.ts new file mode 100644 index 00000000..fe9537a7 --- /dev/null +++ b/angular/src/modules/vault-filter/components/folder-filter.component.ts @@ -0,0 +1,58 @@ +import { Directive, EventEmitter, Input, Output } from "@angular/core"; + +import { ITreeNodeObject } from "jslib-common/models/domain/treeNode"; +import { FolderView } from "jslib-common/models/view/folderView"; + +import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; +import { TopLevelTreeNode } from "../models/top-level-tree-node.model"; +import { VaultFilter } from "../models/vault-filter.model"; + +@Directive() +export class FolderFilterComponent { + @Input() hide = false; + @Input() collapsedFilterNodes: Set; + @Input() folderNodes: DynamicTreeNode; + @Input() activeFilter: VaultFilter; + + @Output() onNodeCollapseStateChange: EventEmitter = + new EventEmitter(); + @Output() onFilterChange: EventEmitter = new EventEmitter(); + @Output() onAddFolder = new EventEmitter(); + @Output() onEditFolder = new EventEmitter(); + + get folders() { + return this.folderNodes?.fullList; + } + + get nestedFolders() { + return this.folderNodes?.nestedList; + } + + readonly foldersGrouping: TopLevelTreeNode = { + id: "folders", + name: "folders", + }; + + applyFilter(folder: FolderView) { + this.activeFilter.resetFilter(); + this.activeFilter.selectedFolder = true; + this.activeFilter.selectedFolderId = folder.id; + this.onFilterChange.emit(this.activeFilter); + } + + addFolder() { + this.onAddFolder.emit(); + } + + editFolder(folder: FolderView) { + this.onEditFolder.emit(folder); + } + + isCollapsed(node: ITreeNodeObject) { + return this.collapsedFilterNodes.has(node.id); + } + + async toggleCollapse(node: ITreeNodeObject) { + this.onNodeCollapseStateChange.emit(node); + } +} diff --git a/angular/src/modules/vault-filter/components/organization-filter.component.ts b/angular/src/modules/vault-filter/components/organization-filter.component.ts new file mode 100644 index 00000000..f03356a8 --- /dev/null +++ b/angular/src/modules/vault-filter/components/organization-filter.component.ts @@ -0,0 +1,84 @@ +import { Directive, EventEmitter, Input, Output } from "@angular/core"; + +import { Organization } from "jslib-common/models/domain/organization"; +import { ITreeNodeObject } from "jslib-common/models/domain/treeNode"; + +import { TopLevelTreeNode } from "../models/top-level-tree-node.model"; +import { VaultFilter } from "../models/vault-filter.model"; + +type DisplayMode = + | "noOrganizations" + | "organizationMember" + | "singleOrganizationPolicy" + | "personalOwnershipPolicy" + | "singleOrganizationAndPersonalOwnershipPolicies"; + +@Directive() +export class OrganizationFilterComponent { + @Input() hide = false; + @Input() collapsedFilterNodes: Set; + @Input() organizations: Organization[]; + @Input() activeFilter: VaultFilter; + @Input() activePersonalOwnershipPolicy: boolean; + @Input() activeSingleOrganizationPolicy: boolean; + + @Output() onNodeCollapseStateChange: EventEmitter = + new EventEmitter(); + @Output() onFilterChange: EventEmitter = new EventEmitter(); + + get displayMode(): DisplayMode { + let displayMode: DisplayMode = "organizationMember"; + if (this.organizations == null || this.organizations.length < 1) { + displayMode = "noOrganizations"; + } else if (this.activePersonalOwnershipPolicy && !this.activeSingleOrganizationPolicy) { + displayMode = "personalOwnershipPolicy"; + } else if (!this.activePersonalOwnershipPolicy && this.activeSingleOrganizationPolicy) { + displayMode = "singleOrganizationPolicy"; + } else if (this.activePersonalOwnershipPolicy && this.activeSingleOrganizationPolicy) { + displayMode = "singleOrganizationAndPersonalOwnershipPolicies"; + } + + return displayMode; + } + + get hasActiveFilter() { + return this.activeFilter.myVaultOnly || this.activeFilter.selectedOrganizationId != null; + } + + readonly organizationGrouping: TopLevelTreeNode = { + id: "vaults", + name: "allVaults", + }; + + async applyOrganizationFilter(organization: Organization) { + this.activeFilter.selectedOrganizationId = organization.id; + this.activeFilter.myVaultOnly = false; + this.activeFilter.refreshCollectionsAndFolders = true; + this.applyFilter(this.activeFilter); + } + + async applyMyVaultFilter() { + this.activeFilter.selectedOrganizationId = null; + this.activeFilter.myVaultOnly = true; + this.activeFilter.refreshCollectionsAndFolders = true; + this.applyFilter(this.activeFilter); + } + + clearFilter() { + this.activeFilter.myVaultOnly = false; + this.activeFilter.selectedOrganizationId = null; + this.applyFilter(new VaultFilter(this.activeFilter)); + } + + private applyFilter(filter: VaultFilter) { + this.onFilterChange.emit(filter); + } + + async toggleCollapse() { + this.onNodeCollapseStateChange.emit(this.organizationGrouping); + } + + get isCollapsed() { + return this.collapsedFilterNodes.has(this.organizationGrouping.id); + } +} diff --git a/angular/src/modules/vault-filter/components/status-filter.component.ts b/angular/src/modules/vault-filter/components/status-filter.component.ts new file mode 100644 index 00000000..fe182ad0 --- /dev/null +++ b/angular/src/modules/vault-filter/components/status-filter.component.ts @@ -0,0 +1,22 @@ +import { Directive, EventEmitter, Input, Output } from "@angular/core"; + +import { CipherStatus } from "../models/cipher-status.model"; +import { VaultFilter } from "../models/vault-filter.model"; + +@Directive() +export class StatusFilterComponent { + @Input() hideFavorites = false; + @Input() hideTrash = false; + @Output() onFilterChange: EventEmitter = new EventEmitter(); + @Input() activeFilter: VaultFilter; + + get show() { + return !this.hideFavorites && !this.hideTrash; + } + + applyFilter(cipherStatus: CipherStatus) { + this.activeFilter.resetFilter(); + this.activeFilter.status = cipherStatus; + this.onFilterChange.emit(this.activeFilter); + } +} diff --git a/angular/src/modules/vault-filter/components/type-filter.component.ts b/angular/src/modules/vault-filter/components/type-filter.component.ts new file mode 100644 index 00000000..49aeaa81 --- /dev/null +++ b/angular/src/modules/vault-filter/components/type-filter.component.ts @@ -0,0 +1,40 @@ +import { Directive, EventEmitter, Input, Output } from "@angular/core"; + +import { CipherType } from "jslib-common/enums/cipherType"; +import { ITreeNodeObject } from "jslib-common/models/domain/treeNode"; + +import { TopLevelTreeNode } from "../models/top-level-tree-node.model"; +import { VaultFilter } from "../models/vault-filter.model"; + +@Directive() +export class TypeFilterComponent { + @Input() hide = false; + @Input() collapsedFilterNodes: Set; + @Input() selectedCipherType: CipherType = null; + @Input() activeFilter: VaultFilter; + + @Output() onNodeCollapseStateChange: EventEmitter = + new EventEmitter(); + @Output() onFilterChange: EventEmitter = new EventEmitter(); + + readonly typesNode: TopLevelTreeNode = { + id: "types", + name: "types", + }; + + cipherTypeEnum = CipherType; // used in the template + + get isCollapsed() { + return this.collapsedFilterNodes.has(this.typesNode.id); + } + + applyFilter(cipherType: CipherType) { + this.activeFilter.resetFilter(); + this.activeFilter.cipherType = cipherType; + this.onFilterChange.emit(this.activeFilter); + } + + async toggleCollapse() { + this.onNodeCollapseStateChange.emit(this.typesNode); + } +} diff --git a/angular/src/modules/vault-filter/models/cipher-status.model.ts b/angular/src/modules/vault-filter/models/cipher-status.model.ts new file mode 100644 index 00000000..f93cd8b2 --- /dev/null +++ b/angular/src/modules/vault-filter/models/cipher-status.model.ts @@ -0,0 +1 @@ +export type CipherStatus = "all" | "favorites" | "trash"; diff --git a/angular/src/modules/vault-filter/models/dynamic-tree-node.model.ts b/angular/src/modules/vault-filter/models/dynamic-tree-node.model.ts new file mode 100644 index 00000000..d63a518f --- /dev/null +++ b/angular/src/modules/vault-filter/models/dynamic-tree-node.model.ts @@ -0,0 +1,16 @@ +import { TreeNode } from "jslib-common/models/domain/treeNode"; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; + +export class DynamicTreeNode { + fullList: T[]; + nestedList: TreeNode[]; + + hasId(id: string): boolean { + return this.fullList != null && this.fullList.filter((i: T) => i.id === id).length > 0; + } + + constructor(init?: Partial>) { + Object.assign(this, init); + } +} diff --git a/angular/src/modules/vault-filter/models/top-level-tree-node.model.ts b/angular/src/modules/vault-filter/models/top-level-tree-node.model.ts new file mode 100644 index 00000000..3e9870de --- /dev/null +++ b/angular/src/modules/vault-filter/models/top-level-tree-node.model.ts @@ -0,0 +1,7 @@ +import { ITreeNodeObject } from "jslib-common/models/domain/treeNode"; + +export type TopLevelTreeNodeId = "vaults" | "types" | "collections" | "folders"; +export class TopLevelTreeNode implements ITreeNodeObject { + id: TopLevelTreeNodeId; + name: string; // localizationString +} diff --git a/angular/src/modules/vault-filter/models/vault-filter.model.ts b/angular/src/modules/vault-filter/models/vault-filter.model.ts new file mode 100644 index 00000000..b65729c5 --- /dev/null +++ b/angular/src/modules/vault-filter/models/vault-filter.model.ts @@ -0,0 +1,32 @@ +import { CipherType } from "jslib-common/enums/cipherType"; + +import { CipherStatus } from "./cipher-status.model"; + +export class VaultFilter { + cipherType?: CipherType; + selectedCollectionId?: string; + status?: CipherStatus; + selectedFolder = false; // This is needed because of how the "No Folder" folder works. It has a null id. + selectedFolderId?: string; + selectedOrganizationId?: string; + myVaultOnly = false; + refreshCollectionsAndFolders = false; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + resetFilter() { + this.cipherType = null; + this.status = null; + this.selectedCollectionId = null; + this.selectedFolder = false; + this.selectedFolderId = null; + } + + resetOrganization() { + this.myVaultOnly = false; + this.selectedOrganizationId = null; + this.resetFilter(); + } +} diff --git a/angular/src/modules/vault-filter/vault-filter.component.ts b/angular/src/modules/vault-filter/vault-filter.component.ts new file mode 100644 index 00000000..d2c2b6c0 --- /dev/null +++ b/angular/src/modules/vault-filter/vault-filter.component.ts @@ -0,0 +1,95 @@ +import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; + +import { Organization } from "jslib-common/models/domain/organization"; +import { ITreeNodeObject } from "jslib-common/models/domain/treeNode"; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; + +import { DynamicTreeNode } from "./models/dynamic-tree-node.model"; +import { VaultFilter } from "./models/vault-filter.model"; +import { VaultFilterService } from "./vault-filter.service"; + +@Directive() +export class VaultFilterComponent implements OnInit { + @Input() activeFilter: VaultFilter = new VaultFilter(); + @Input() hideFolders = false; + @Input() hideCollections = false; + @Input() hideFavorites = false; + @Input() hideTrash = false; + @Input() hideOrganizations = false; + + @Output() onFilterChange = new EventEmitter(); + + isLoaded = false; + collapsedFilterNodes: Set; + organizations: Organization[]; + activePersonalOwnershipPolicy: boolean; + activeSingleOrganizationPolicy: boolean; + collections: DynamicTreeNode; + folders: DynamicTreeNode; + + constructor(protected vaultFilterService: VaultFilterService) {} + + get displayCollections() { + return this.collections?.fullList != null && this.collections.fullList.length > 0; + } + + async ngOnInit(): Promise { + this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes(); + this.organizations = await this.vaultFilterService.buildOrganizations(); + if (this.organizations != null && this.organizations.length > 0) { + this.activePersonalOwnershipPolicy = + await this.vaultFilterService.checkForPersonalOwnershipPolicy(); + this.activeSingleOrganizationPolicy = + await this.vaultFilterService.checkForSingleOrganizationPolicy(); + } + this.folders = await this.vaultFilterService.buildFolders(); + this.collections = await this.vaultFilterService.buildCollections(); + this.isLoaded = true; + } + + async toggleFilterNodeCollapseState(node: ITreeNodeObject) { + if (this.collapsedFilterNodes.has(node.id)) { + this.collapsedFilterNodes.delete(node.id); + } else { + this.collapsedFilterNodes.add(node.id); + } + await this.vaultFilterService.storeCollapsedFilterNodes(this.collapsedFilterNodes); + } + + async applyFilter(filter: VaultFilter) { + if (filter.refreshCollectionsAndFolders) { + await this.reloadCollectionsAndFolders(filter); + } + this.fixInvalidFilterSelections(filter); + this.onFilterChange.emit(filter); + } + + async reloadCollectionsAndFolders(filter: VaultFilter) { + this.folders = await this.vaultFilterService.buildFolders(filter.selectedOrganizationId); + this.collections = filter.myVaultOnly + ? null + : await this.vaultFilterService.buildCollections(filter.selectedOrganizationId); + } + + protected fixInvalidFilterSelections(filter: VaultFilter) { + this.fixInvalidFolderSelection(filter); + this.fixInvalidCollectionSelection(filter); + } + + protected fixInvalidFolderSelection(filter: VaultFilter) { + if (filter.selectedFolder && !this.folders.hasId(filter.selectedFolderId)) { + this.activeFilter.selectedFolder = false; + this.activeFilter.selectedFolderId = null; + } + } + + protected fixInvalidCollectionSelection(filter: VaultFilter) { + if ( + filter.selectedCollectionId != null && + !this.collections.hasId(filter.selectedCollectionId) + ) { + this.activeFilter.selectedCollectionId = null; + } + } +} diff --git a/angular/src/modules/vault-filter/vault-filter.service.ts b/angular/src/modules/vault-filter/vault-filter.service.ts new file mode 100644 index 00000000..38a6683b --- /dev/null +++ b/angular/src/modules/vault-filter/vault-filter.service.ts @@ -0,0 +1,79 @@ +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { PolicyType } from "jslib-common/enums/policyType"; +import { Organization } from "jslib-common/models/domain/organization"; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; + +import { DynamicTreeNode } from "./models/dynamic-tree-node.model"; + +export class VaultFilterService { + constructor( + protected stateService: StateService, + protected organizationService: OrganizationService, + protected folderService: FolderService, + protected cipherService: CipherService, + protected collectionService: CollectionService, + protected policyService: PolicyService + ) {} + + async storeCollapsedFilterNodes(collapsedFilterNodes: Set): Promise { + await this.stateService.setCollapsedGroupings(Array.from(collapsedFilterNodes)); + } + + async buildCollapsedFilterNodes(): Promise> { + return new Set(await this.stateService.getCollapsedGroupings()); + } + + async buildOrganizations(): Promise { + return await this.organizationService.getAll(); + } + + async buildFolders(organizationId?: string): Promise> { + const storedFolders = await this.folderService.getAllDecrypted(); + let folders: FolderView[]; + if (organizationId != null) { + const ciphers = await this.cipherService.getAllDecrypted(); + const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId); + folders = storedFolders.filter( + (f) => + orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 || + ciphers.filter((c) => c.folderId == f.id).length < 1 + ); + } else { + folders = storedFolders; + } + const nestedFolders = await this.folderService.getAllNested(folders); + return new DynamicTreeNode({ + fullList: folders, + nestedList: nestedFolders, + }); + } + + async buildCollections(organizationId?: string): Promise> { + const storedCollections = await this.collectionService.getAllDecrypted(); + let collections: CollectionView[]; + if (organizationId != null) { + collections = storedCollections.filter((c) => c.organizationId === organizationId); + } else { + collections = storedCollections; + } + const nestedCollections = await this.collectionService.getAllNested(collections); + return new DynamicTreeNode({ + fullList: collections, + nestedList: nestedCollections, + }); + } + + async checkForSingleOrganizationPolicy(): Promise { + return await this.policyService.policyAppliesToUser(PolicyType.SingleOrg); + } + + async checkForPersonalOwnershipPolicy(): Promise { + return await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership); + } +}