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