1
0
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:
Addison Beck
2022-08-08 15:08:35 -04:00
committed by GitHub
parent 56ce85c69c
commit 95bb429281
120 changed files with 289 additions and 293 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1 @@
export type CipherStatus = "all" | "favorites" | "trash";

View File

@@ -0,0 +1,6 @@
export type DisplayMode =
| "noOrganizations"
| "organizationMember"
| "singleOrganizationPolicy"
| "personalOwnershipPolicy"
| "singleOrganizationAndPersonalOwnershipPolicies";

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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;
};
}
}

View File

@@ -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>;
}
}