mirror of
https://github.com/bitwarden/jslib
synced 2025-12-22 11:13:17 +00:00
[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.
This commit is contained in:
@@ -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<CipherType>();
|
||||
@Output() onFolderClicked = new EventEmitter<FolderView>();
|
||||
@Output() onAddFolder = new EventEmitter();
|
||||
@Output() onEditFolder = new EventEmitter<FolderView>();
|
||||
@Output() onCollectionClicked = new EventEmitter<CollectionView>();
|
||||
@Output() onOrganizationClicked = new EventEmitter<Organization>();
|
||||
@Output() onMyVaultClicked = new EventEmitter();
|
||||
@Output() onAllVaultsClicked = new EventEmitter();
|
||||
|
||||
folders: FolderView[];
|
||||
nestedFolders: TreeNode<FolderView>[];
|
||||
collections: CollectionView[];
|
||||
nestedCollections: TreeNode<CollectionView>[];
|
||||
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<string>;
|
||||
|
||||
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<string>();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@@ -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<string>;
|
||||
@Input() collectionNodes: DynamicTreeNode<CollectionView>;
|
||||
@Input() activeFilter: VaultFilter;
|
||||
|
||||
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
|
||||
new EventEmitter<ITreeNodeObject>();
|
||||
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string>;
|
||||
@Input() folderNodes: DynamicTreeNode<FolderView>;
|
||||
@Input() activeFilter: VaultFilter;
|
||||
|
||||
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
|
||||
new EventEmitter<ITreeNodeObject>();
|
||||
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
|
||||
@Output() onAddFolder = new EventEmitter();
|
||||
@Output() onEditFolder = new EventEmitter<FolderView>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string>;
|
||||
@Input() organizations: Organization[];
|
||||
@Input() activeFilter: VaultFilter;
|
||||
@Input() activePersonalOwnershipPolicy: boolean;
|
||||
@Input() activeSingleOrganizationPolicy: boolean;
|
||||
|
||||
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
|
||||
new EventEmitter<ITreeNodeObject>();
|
||||
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<VaultFilter> = new EventEmitter<VaultFilter>();
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<string>;
|
||||
@Input() selectedCipherType: CipherType = null;
|
||||
@Input() activeFilter: VaultFilter;
|
||||
|
||||
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
|
||||
new EventEmitter<ITreeNodeObject>();
|
||||
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type CipherStatus = "all" | "favorites" | "trash";
|
||||
@@ -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<T extends CollectionView | FolderView> {
|
||||
fullList: T[];
|
||||
nestedList: TreeNode<T>[];
|
||||
|
||||
hasId(id: string): boolean {
|
||||
return this.fullList != null && this.fullList.filter((i: T) => i.id === id).length > 0;
|
||||
}
|
||||
|
||||
constructor(init?: Partial<DynamicTreeNode<T>>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<VaultFilter>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
95
angular/src/modules/vault-filter/vault-filter.component.ts
Normal file
95
angular/src/modules/vault-filter/vault-filter.component.ts
Normal file
@@ -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<VaultFilter>();
|
||||
|
||||
isLoaded = false;
|
||||
collapsedFilterNodes: Set<string>;
|
||||
organizations: Organization[];
|
||||
activePersonalOwnershipPolicy: boolean;
|
||||
activeSingleOrganizationPolicy: boolean;
|
||||
collections: DynamicTreeNode<CollectionView>;
|
||||
folders: DynamicTreeNode<FolderView>;
|
||||
|
||||
constructor(protected vaultFilterService: VaultFilterService) {}
|
||||
|
||||
get displayCollections() {
|
||||
return this.collections?.fullList != null && this.collections.fullList.length > 0;
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
angular/src/modules/vault-filter/vault-filter.service.ts
Normal file
79
angular/src/modules/vault-filter/vault-filter.service.ts
Normal file
@@ -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<string>): Promise<void> {
|
||||
await this.stateService.setCollapsedGroupings(Array.from(collapsedFilterNodes));
|
||||
}
|
||||
|
||||
async buildCollapsedFilterNodes(): Promise<Set<string>> {
|
||||
return new Set(await this.stateService.getCollapsedGroupings());
|
||||
}
|
||||
|
||||
async buildOrganizations(): Promise<Organization[]> {
|
||||
return await this.organizationService.getAll();
|
||||
}
|
||||
|
||||
async buildFolders(organizationId?: string): Promise<DynamicTreeNode<FolderView>> {
|
||||
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<FolderView>({
|
||||
fullList: folders,
|
||||
nestedList: nestedFolders,
|
||||
});
|
||||
}
|
||||
|
||||
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
|
||||
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<CollectionView>({
|
||||
fullList: collections,
|
||||
nestedList: nestedCollections,
|
||||
});
|
||||
}
|
||||
|
||||
async checkForSingleOrganizationPolicy(): Promise<boolean> {
|
||||
return await this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
|
||||
}
|
||||
|
||||
async checkForPersonalOwnershipPolicy(): Promise<boolean> {
|
||||
return await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user