mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[SG-360] Remove the /modules/ folder (#3225)
* Move Web's SharedModule to /app/shared/ This commit relocates `SharedModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `SharedModule`. * Move /modules/pipes to /shared/pipes This commit relocates `PipesModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `PipesModule`. * Move LooseComponentsModule to /shared/ This commit relocates `LooseComponentsModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `LooseComponentsModule`. * Move VerticalStepperModule to /shared/ This commit relocates `VerticalStepperModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `VerticalStepperModule`. * Move TrialInitiationModule to /shared/ This commit relocates `TrialInitiationModule` & `RegisterFormModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `TrialInitiationModule` or `RegisterFormModule`. * Move /modules/organization to /organization This commit relocates all modules in `/app/modules/organization` to `/app/organization` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Move /modules/vault/ to /vault This commit relocates the IndividualVaultModule to `/app/modules/vault`, and the OrganizationVaultModule to `/app/organization/vault` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Move VaultFiltersModule to /vault This commit relocates the `VaultFilterModule` to `/app/vault/vault-filter`, and the OrganizationVaultFilterComponent to `/app/organization/vault/vault-filter` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Remove the /modules/ folder from desktop This commit relocates the `VaultFilterModule` to `/app/vault/vault-filter`, and the OrganizationVaultFilterComponent to `/app/organization/vault/vault-filter` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Move Libs' VaultFiltersComponent to /vault/ This commit moves the lib's logic for `VaultFiltersModule` from `/modules/` to `/vault/` All other changes are just to adjust imports that reference the moved files. * Rename VaultModule -> SharedVaultModule * Rename IndividualVaultModule -> VaultModule * Rename OrganizationVaultModule -> VaultModule * Rename OrganizationVaultFilterComponent Rename OrganizationVaultFilterComponent to VaultFilterComponent * Seperate the two VaultFilterComponents This commit seperate the `OrganizationVaultFilterComponent` from the `VaultFilerModule`, which is only used by the individual vault. A `VaultFilterSharedModule` was created to declare shared components and provide shared services between the two implementations. This was done to align with best practices for NgModules. * [r] Move VerticalStepperModule to /account/ More specifically, /account/trial/ * [r] Declare PaymentComponent in LooseComponentsModule `PaymentComponent` is not reused across domains and should not be declared in `SharedModule`. I've moved it to `LooseComponentsModule` for now, but later it will need to be exported from a `SettingsModule`. * [r] Declare TaxInfoComponent in LooseComponentsModule * [r] Reloacte Pipes out of /shared/ * [r] Extract locales out of SharedModule * [r] Add documentation to shared module * [r] Cleanup imports * [r] Use an index.ts file for /shared/ * [r] Add eslint rule restricting access to /shared/ Co-authored-by: Hinton <hinton@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/treeNode";
|
||||
import { CollectionView } from "@bitwarden/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.selectedCollection = true;
|
||||
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 "@bitwarden/common/models/domain/treeNode";
|
||||
import { FolderView } from "@bitwarden/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,78 @@
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/treeNode";
|
||||
|
||||
import { DisplayMode } from "../models/display-mode";
|
||||
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
|
||||
import { VaultFilter } from "../models/vault-filter.model";
|
||||
|
||||
@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 "@bitwarden/common/enums/cipherType";
|
||||
import { ITreeNodeObject } from "@bitwarden/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,128 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/treeNode";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
import { VaultFilter } from "../models/vault-filter.model";
|
||||
import { VaultFilterService } from "../services/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>();
|
||||
@Output() onAddFolder = new EventEmitter<never>();
|
||||
@Output() onEditFolder = new EventEmitter<FolderView>();
|
||||
|
||||
isLoaded = false;
|
||||
collapsedFilterNodes: Set<string>;
|
||||
organizations: Organization[];
|
||||
activePersonalOwnershipPolicy: boolean;
|
||||
activeSingleOrganizationPolicy: boolean;
|
||||
collections: DynamicTreeNode<CollectionView>;
|
||||
folders$: Observable<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.buildNestedFolders();
|
||||
this.collections = await this.initCollections();
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
// overwritten in web for organization vaults
|
||||
async initCollections() {
|
||||
return await this.vaultFilterService.buildCollections();
|
||||
}
|
||||
|
||||
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);
|
||||
filter = await this.pruneInvalidatedFilterSelections(filter);
|
||||
}
|
||||
this.onFilterChange.emit(filter);
|
||||
}
|
||||
|
||||
async reloadCollectionsAndFolders(filter: VaultFilter) {
|
||||
this.folders$ = await this.vaultFilterService.buildNestedFolders(filter.selectedOrganizationId);
|
||||
this.collections = filter.myVaultOnly
|
||||
? null
|
||||
: await this.vaultFilterService.buildCollections(filter.selectedOrganizationId);
|
||||
}
|
||||
|
||||
async reloadOrganizations() {
|
||||
this.organizations = await this.vaultFilterService.buildOrganizations();
|
||||
this.activePersonalOwnershipPolicy =
|
||||
await this.vaultFilterService.checkForPersonalOwnershipPolicy();
|
||||
this.activeSingleOrganizationPolicy =
|
||||
await this.vaultFilterService.checkForSingleOrganizationPolicy();
|
||||
}
|
||||
|
||||
addFolder() {
|
||||
this.onAddFolder.emit();
|
||||
}
|
||||
|
||||
editFolder(folder: FolderView) {
|
||||
this.onEditFolder.emit(folder);
|
||||
}
|
||||
|
||||
protected async pruneInvalidatedFilterSelections(filter: VaultFilter): Promise<VaultFilter> {
|
||||
filter = await this.pruneInvalidFolderSelection(filter);
|
||||
filter = this.pruneInvalidCollectionSelection(filter);
|
||||
return filter;
|
||||
}
|
||||
|
||||
protected async pruneInvalidFolderSelection(filter: VaultFilter): Promise<VaultFilter> {
|
||||
if (
|
||||
filter.selectedFolder &&
|
||||
!(await firstValueFrom(this.folders$))?.hasId(filter.selectedFolderId)
|
||||
) {
|
||||
filter.selectedFolder = false;
|
||||
filter.selectedFolderId = null;
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
protected pruneInvalidCollectionSelection(filter: VaultFilter): VaultFilter {
|
||||
if (
|
||||
filter.myVaultOnly ||
|
||||
(filter.selectedCollection &&
|
||||
filter.selectedCollectionId != null &&
|
||||
!this.collections?.hasId(filter.selectedCollectionId))
|
||||
) {
|
||||
filter.selectedCollection = false;
|
||||
filter.selectedCollectionId = null;
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type CipherStatus = "all" | "favorites" | "trash";
|
||||
@@ -0,0 +1,6 @@
|
||||
export type DisplayMode =
|
||||
| "noOrganizations"
|
||||
| "organizationMember"
|
||||
| "singleOrganizationPolicy"
|
||||
| "personalOwnershipPolicy"
|
||||
| "singleOrganizationAndPersonalOwnershipPolicies";
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||
import { FolderView } from "@bitwarden/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 "@bitwarden/common/models/domain/treeNode";
|
||||
|
||||
export type TopLevelTreeNodeId = "vaults" | "types" | "collections" | "folders";
|
||||
export class TopLevelTreeNode implements ITreeNodeObject {
|
||||
id: TopLevelTreeNodeId;
|
||||
name: string; // localizationString
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||
|
||||
import { VaultFilter } from "./vault-filter.model";
|
||||
|
||||
describe("VaultFilter", () => {
|
||||
describe("filterFunction", () => {
|
||||
describe("generic cipher", () => {
|
||||
it("should return true when not filtering for anything specific", () => {
|
||||
const cipher = createCipher();
|
||||
const filterFunction = createFilterFunction({ status: "all" });
|
||||
|
||||
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({ status: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ status: "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({ status: "trash" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ status: "favorites" });
|
||||
|
||||
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({ cipherType: CipherType.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({ cipherType: CipherType.Identity });
|
||||
|
||||
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({
|
||||
selectedFolder: true,
|
||||
selectedFolderId: "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({
|
||||
selectedFolder: true,
|
||||
selectedFolderId: "anotherFolderId",
|
||||
});
|
||||
|
||||
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({
|
||||
selectedFolder: true,
|
||||
selectedFolderId: 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({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: "collectionId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: "nonMatchingId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationId: "anotherOrganizationId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering for my vault only", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
myVaultOnly: true,
|
||||
});
|
||||
|
||||
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({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: null,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when filter matches organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationId: "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({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: 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({
|
||||
myVaultOnly: true,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFilterFunction(options: Partial<VaultFilter> = {}) {
|
||||
return new VaultFilter(options).buildFilter();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||
|
||||
import { CipherStatus } from "./cipher-status.model";
|
||||
|
||||
export type VaultFilterFunction = (cipher: CipherView) => boolean;
|
||||
|
||||
export class VaultFilter {
|
||||
cipherType?: CipherType;
|
||||
selectedCollection = false; // This is needed because of how the "Unassigned" collection works. It has a null id.
|
||||
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.selectedCollection = false;
|
||||
this.selectedCollectionId = null;
|
||||
this.selectedFolder = false;
|
||||
this.selectedFolderId = null;
|
||||
}
|
||||
|
||||
resetOrganization() {
|
||||
this.myVaultOnly = false;
|
||||
this.selectedOrganizationId = null;
|
||||
this.resetFilter();
|
||||
}
|
||||
|
||||
buildFilter(): VaultFilterFunction {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.status === "favorites" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.favorite;
|
||||
}
|
||||
if (this.status === "trash" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.cipherType != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.cipherType;
|
||||
}
|
||||
if (this.selectedFolder && this.selectedFolderId == null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId == null;
|
||||
}
|
||||
if (this.selectedFolder && this.selectedFolderId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === this.selectedFolderId;
|
||||
}
|
||||
if (this.selectedCollection && this.selectedCollectionId == null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.organizationId != null &&
|
||||
(cipher.collectionIds == null || cipher.collectionIds.length === 0);
|
||||
}
|
||||
if (this.selectedCollection && this.selectedCollectionId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null && cipher.collectionIds.includes(this.selectedCollectionId);
|
||||
}
|
||||
if (this.selectedOrganizationId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.selectedOrganizationId;
|
||||
}
|
||||
if (this.myVaultOnly && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, from, mergeMap, Observable } from "rxjs";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
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();
|
||||
}
|
||||
|
||||
buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> {
|
||||
const transformation = async (storedFolders: FolderView[]) => {
|
||||
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.getAllFoldersNested(folders);
|
||||
return new DynamicTreeNode<FolderView>({
|
||||
fullList: folders,
|
||||
nestedList: nestedFolders,
|
||||
});
|
||||
};
|
||||
|
||||
return this.folderService.folderViews$.pipe(
|
||||
mergeMap((folders) => from(transformation(folders)))
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected async getAllFoldersNested(folders: FolderView[]): Promise<TreeNode<FolderView>[]> {
|
||||
const nodes: TreeNode<FolderView>[] = [];
|
||||
folders.forEach((f) => {
|
||||
const folderCopy = new FolderView();
|
||||
folderCopy.id = f.id;
|
||||
folderCopy.revisionDate = f.revisionDate;
|
||||
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async getFolderNested(id: string): Promise<TreeNode<FolderView>> {
|
||||
const folders = await this.getAllFoldersNested(
|
||||
await firstValueFrom(this.folderService.folderViews$)
|
||||
);
|
||||
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user