diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 1b9a6821d5..a3e2cc8cdf 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -1,14 +1,12 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs"; +import { firstValueFrom, Subject } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/enums/policyType"; import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; -import { CollectionView } from "@bitwarden/common/models/view/collection.view"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; -import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { VaultFilterService } from "../services/abstractions/vault-filter.service"; import { @@ -34,7 +32,6 @@ import { OrganizationOptionsComponent } from "./organization-options.component"; export class VaultFilterComponent implements OnInit, OnDestroy { filters?: VaultFilterList; @Input() activeFilter: VaultFilter = new VaultFilter(); - @Output() activeFilterChanged = new EventEmitter(); @Output() onSearchTextChanged = new EventEmitter(); @Output() onAddFolder = new EventEmitter(); @Output() onEditFolder = new EventEmitter(); @@ -88,9 +85,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService - ) { - this.loadSubscriptions(); - } + ) {} async ngOnInit(): Promise { this.filters = await this.buildAllFilters(); @@ -104,35 +99,11 @@ export class VaultFilterComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - protected loadSubscriptions() { - this.vaultFilterService.filteredFolders$ - .pipe( - switchMap(async (folders) => { - this.removeInvalidFolderSelection(folders); - }), - takeUntil(this.destroy$) - ) - .subscribe(); - - this.vaultFilterService.filteredCollections$ - .pipe( - switchMap(async (collections) => { - this.removeInvalidCollectionSelection(collections); - }), - takeUntil(this.destroy$) - ) - .subscribe(); - } - searchTextChanged(t: string) { this.searchText = t; this.onSearchTextChanged.emit(t); } - protected applyVaultFilter(filter: VaultFilter) { - this.activeFilterChanged.emit(filter); - } - applyOrganizationFilter = async (orgNode: TreeNode): Promise => { if (!orgNode?.node.enabled) { this.platformUtilsService.showToast( @@ -143,34 +114,31 @@ export class VaultFilterComponent implements OnInit, OnDestroy { return; } const filter = this.activeFilter; - filter.resetOrganization(); - if (orgNode?.node.id !== "AllVaults") { + if (orgNode?.node.id === "AllVaults") { + filter.resetOrganization(); + } else { filter.selectedOrganizationNode = orgNode; } this.vaultFilterService.setOrganizationFilter(orgNode.node); await this.vaultFilterService.expandOrgFilter(); - this.applyVaultFilter(filter); }; applyTypeFilter = async (filterNode: TreeNode): Promise => { const filter = this.activeFilter; filter.resetFilter(); filter.selectedCipherTypeNode = filterNode; - this.applyVaultFilter(filter); }; applyFolderFilter = async (folderNode: TreeNode): Promise => { const filter = this.activeFilter; filter.resetFilter(); filter.selectedFolderNode = folderNode; - this.applyVaultFilter(filter); }; applyCollectionFilter = async (collectionNode: TreeNode): Promise => { const filter = this.activeFilter; filter.resetFilter(); filter.selectedCollectionNode = collectionNode; - this.applyVaultFilter(filter); }; addFolder = async (): Promise => { @@ -185,30 +153,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { return await firstValueFrom(this.filters?.typeFilter.data$); } - protected async removeInvalidFolderSelection(folders: FolderView[]) { - if (this.activeFilter.selectedFolderNode) { - if (!folders.some((f) => f.id === this.activeFilter.folderId)) { - const filter = this.activeFilter; - filter.resetFilter(); - filter.selectedCipherTypeNode = - (await this.getDefaultFilter()) as TreeNode; - this.applyVaultFilter(filter); - } - } - } - - protected async removeInvalidCollectionSelection(collections: CollectionView[]) { - if (this.activeFilter.selectedCollectionNode) { - if (!collections.some((f) => f.id === this.activeFilter.collectionId)) { - const filter = this.activeFilter; - filter.resetFilter(); - filter.selectedCipherTypeNode = - (await this.getDefaultFilter()) as TreeNode; - this.applyVaultFilter(filter); - } - } - } - async buildAllFilters(): Promise { const builderFilter = {} as VaultFilterList; builderFilter.organizationFilter = await this.addOrganizationFilter(); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index 7ae84244a8..3a454d08d5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -19,6 +19,7 @@ export abstract class VaultFilterService { organizationTree$: Observable>; folderTree$: Observable>; collectionTree$: Observable>; + cipherTypeTree$: Observable>; reloadCollections: () => Promise; getCollectionNodeFromTree: (id: string) => Promise>; setCollapsedFilterNodes: (collapsedFilterNodes: Set) => Promise; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts new file mode 100644 index 0000000000..a532ecff78 --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts @@ -0,0 +1,179 @@ +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { combineLatest, map, Observable } from "rxjs"; + +import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils"; +import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; + +import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model"; +import { + RoutedVaultFilterModel, + Unassigned, + All, +} from "../shared/models/routed-vault-filter.model"; +import { VaultFilter } from "../shared/models/vault-filter.model"; +import { + CipherTypeFilter, + CollectionFilter, + FolderFilter, + OrganizationFilter, +} from "../shared/models/vault-filter.type"; + +import { VaultFilterService } from "./abstractions/vault-filter.service"; +import { RoutedVaultFilterService } from "./routed-vault-filter.service"; + +/** + * This file is part of a layer that is used to temporary bridge between URL filtering and the old state-in-code method. + * This should be removed after we have refactored the {@link VaultItemsComponent} and introduced vertical navigation + * (which will refactor the {@link VaultFilterComponent}). + * + * This class listens to both the new {@link RoutedVaultFilterService} and the old {@link VaultFilterService}. + * When a new filter is emitted the service uses the ids to find the corresponding tree nodes needed for + * the old {@link VaultFilter} model. It then emits a bridge model that contains this information. + */ +@Injectable() +export class RoutedVaultFilterBridgeService { + readonly activeFilter$: Observable; + + constructor( + private router: Router, + private routedVaultFilterService: RoutedVaultFilterService, + legacyVaultFilterService: VaultFilterService + ) { + this.activeFilter$ = combineLatest([ + routedVaultFilterService.filter$, + legacyVaultFilterService.collectionTree$, + legacyVaultFilterService.folderTree$, + legacyVaultFilterService.organizationTree$, + legacyVaultFilterService.cipherTypeTree$, + ]).pipe( + map(([filter, collectionTree, folderTree, organizationTree, cipherTypeTree]) => { + const legacyFilter = isAdminConsole(filter) + ? createLegacyFilterForAdminConsole(filter, collectionTree, cipherTypeTree) + : createLegacyFilterForEndUser( + filter, + collectionTree, + folderTree, + organizationTree, + cipherTypeTree + ); + + return new RoutedVaultFilterBridge(filter, legacyFilter, this); + }) + ); + } + + navigate(filter: RoutedVaultFilterModel) { + const [commands, extras] = this.routedVaultFilterService.createRoute(filter); + this.router.navigate(commands, extras); + } +} + +/** + * Check if the filtering is being done as part of admin console. + * Admin console can be identified by checking if the `organizationId` + * is part of the path. + * + * @param filter Model to check if origin is admin console + * @returns true if filtering being done as part of admin console + */ +function isAdminConsole(filter: RoutedVaultFilterModel) { + return filter.organizationIdParamType === "path"; +} + +function createLegacyFilterForAdminConsole( + filter: RoutedVaultFilterModel, + collectionTree: TreeNode, + cipherTypeTree: TreeNode +): VaultFilter { + const legacyFilter = new VaultFilter(); + + if (filter.collectionId === undefined && filter.type === undefined) { + legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject( + collectionTree, + "AllCollections" + ); + } else if (filter.collectionId !== undefined && filter.collectionId === Unassigned) { + legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(collectionTree, null); + } else if (filter.collectionId !== undefined) { + legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject( + collectionTree, + filter.collectionId + ); + } + + if (filter.collectionId === undefined && filter.type === All) { + legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject( + cipherTypeTree, + "AllItems" + ); + } else if (filter.type !== undefined && filter.type === "trash") { + legacyFilter.selectedCipherTypeNode = new TreeNode( + { id: "trash", name: "", type: "trash", icon: "" }, + null + ); + } else if (filter.type !== undefined && filter.type !== "trash") { + legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject( + cipherTypeTree, + filter.type + ); + } + + return legacyFilter; +} + +function createLegacyFilterForEndUser( + filter: RoutedVaultFilterModel, + collectionTree: TreeNode, + folderTree: TreeNode, + organizationTree: TreeNode, + cipherTypeTree: TreeNode +): VaultFilter { + const legacyFilter = new VaultFilter(); + + if (filter.collectionId !== undefined && filter.collectionId === Unassigned) { + legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(collectionTree, null); + } else if (filter.collectionId !== undefined && filter.collectionId === All) { + legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject( + collectionTree, + "AllCollections" + ); + } else if (filter.collectionId !== undefined) { + legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject( + collectionTree, + filter.collectionId + ); + } + + if (filter.folderId !== undefined && filter.folderId === Unassigned) { + legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, null); + } else if (filter.folderId !== undefined && filter.folderId !== Unassigned) { + legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, filter.folderId); + } + + if (filter.organizationId !== undefined) { + legacyFilter.selectedOrganizationNode = ServiceUtils.getTreeNodeObject( + organizationTree, + filter.organizationId + ); + } + + if (filter.type === undefined) { + legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject( + cipherTypeTree, + "AllItems" + ); + } else if (filter.type !== undefined && filter.type === "trash") { + legacyFilter.selectedCipherTypeNode = new TreeNode( + { id: "trash", name: "", type: "trash", icon: "" }, + null + ); + } else if (filter.type !== undefined && filter.type !== "trash") { + legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject( + cipherTypeTree, + filter.type + ); + } + + return legacyFilter; +} diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service.ts new file mode 100644 index 0000000000..26578605dc --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service.ts @@ -0,0 +1,81 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { ActivatedRoute, NavigationExtras } from "@angular/router"; +import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs"; + +import { + isRoutedVaultFilterItemType, + RoutedVaultFilterModel, +} from "../shared/models/routed-vault-filter.model"; + +/** + * This service is an abstraction layer on top of ActivatedRoute that + * encapsulates the logic of how filters are stored in the URL. + * + * The service builds and emits filter models based on URL params and + * also contains a method for generating routes to corresponding to those params. + */ +@Injectable() +export class RoutedVaultFilterService implements OnDestroy { + private onDestroy = new Subject(); + + /** + * Filter values extracted from the URL. + * To change the values use {@link RoutedVaultFilterService.createRoute}. + */ + filter$: Observable; + + constructor(activatedRoute: ActivatedRoute) { + this.filter$ = combineLatest([activatedRoute.paramMap, activatedRoute.queryParamMap]).pipe( + map(([params, queryParams]) => { + const unsafeType = queryParams.get("type"); + const type = isRoutedVaultFilterItemType(unsafeType) ? unsafeType : undefined; + + return { + collectionId: queryParams.get("collectionId") ?? undefined, + folderId: queryParams.get("folderId") ?? undefined, + organizationId: + params.get("organizationId") ?? queryParams.get("organizationId") ?? undefined, + organizationIdParamType: + params.get("organizationId") != undefined ? ("path" as const) : ("query" as const), + type, + }; + }), + takeUntil(this.onDestroy) + ); + } + + /** + * Create a route that can be used to modify filters with Router or RouterLink. + * This method is specifically built to leave other query parameters untouched, + * meaning that navigation will only affect filters and not e.g. `cipherId`. + * To subscribe to changes use {@link RoutedVaultFilterService.filter$}. + * + * Note: + * This method currently only supports changing filters that are stored + * in query parameters. This means that {@link RoutedVaultFilterModel.organizationId} + * will be ignored if {@link RoutedVaultFilterModel.organizationIdParamType} + * is set to `path`. + * + * @param filter Filter values that should be applied to the URL. + * @returns route that can be used with Router or RouterLink + */ + createRoute(filter: RoutedVaultFilterModel): [commands: any[], extras?: NavigationExtras] { + const commands: string[] = []; + const extras: NavigationExtras = { + queryParams: { + collectionId: filter.collectionId ?? null, + folderId: filter.folderId ?? null, + organizationId: + filter.organizationIdParamType === "path" ? null : filter.organizationId ?? null, + type: filter.type ?? null, + }, + queryParamsHandling: "merge", + }; + return [commands, extras]; + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } +} diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index ef040f02ad..d07648a531 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -22,6 +22,7 @@ import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; import { CollectionView } from "@bitwarden/common/models/view/collection.view"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CollectionAdminView } from "../../../../organizations/core"; @@ -73,6 +74,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { map((collections) => this.buildCollectionTree(collections)) ); + cipherTypeTree$: Observable> = this.buildCipherTypeTree(); + constructor( protected stateService: StateService, protected organizationService: OrganizationService, @@ -254,4 +257,44 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { const head = new FolderView() as FolderFilter; return new TreeNode(head, null, "folders", "AllFolders"); } + + protected buildCipherTypeTree(): Observable> { + const allTypeFilters: CipherTypeFilter[] = [ + { + id: "favorites", + name: this.i18nService.t("favorites"), + type: "favorites", + icon: "bwi-star", + }, + { + id: "login", + name: this.i18nService.t("typeLogin"), + type: CipherType.Login, + icon: "bwi-globe", + }, + { + id: "card", + name: this.i18nService.t("typeCard"), + type: CipherType.Card, + icon: "bwi-credit-card", + }, + { + id: "identity", + name: this.i18nService.t("typeIdentity"), + type: CipherType.Identity, + icon: "bwi-id-card", + }, + { + id: "note", + name: this.i18nService.t("typeSecureNote"), + type: CipherType.SecureNote, + icon: "bwi-sticky-note", + }, + ]; + + return this.buildTypeTree( + { id: "AllItems", name: "allItems", type: "all", icon: "" }, + allTypeFilters + ); + } } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts new file mode 100644 index 0000000000..c2a25b6751 --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts @@ -0,0 +1,168 @@ +import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; + +import { RoutedVaultFilterBridgeService } from "../../services/routed-vault-filter-bridge.service"; + +import { + All, + isRoutedVaultFilterItemType, + RoutedVaultFilterItemType, + RoutedVaultFilterModel, + Unassigned, +} from "./routed-vault-filter.model"; +import { VaultFilter, VaultFilterFunction } from "./vault-filter.model"; +import { + OrganizationFilter, + CipherTypeFilter, + FolderFilter, + CollectionFilter, + CipherStatus, +} from "./vault-filter.type"; + +/** + * This file is part of a layer that is used to temporary bridge between URL filtering and the old state-in-code method. + * This should be removed after we have refactored the {@link VaultItemsComponent} and introduced vertical navigation + * (which will refactor the {@link VaultFilterComponent}). + * + * This model supplies legacy code with the old state-in-code models saved as tree nodes. + * It can also receive requests to select a new tree node by using setters. + * However instead of just replacing the tree node models, it requests a URL navigation, + * thereby bridging between legacy and URL filtering. + */ +export class RoutedVaultFilterBridge implements VaultFilter { + constructor( + private routedFilter: RoutedVaultFilterModel, + private legacyFilter: VaultFilter, + private bridgeService: RoutedVaultFilterBridgeService + ) {} + get collectionBreadcrumbs(): TreeNode[] { + return this.legacyFilter.collectionBreadcrumbs; + } + get isCollectionSelected(): boolean { + return this.legacyFilter.isCollectionSelected; + } + get isUnassignedCollectionSelected(): boolean { + return this.legacyFilter.isUnassignedCollectionSelected; + } + get isMyVaultSelected(): boolean { + return this.legacyFilter.isMyVaultSelected; + } + get selectedOrganizationNode(): TreeNode { + return this.legacyFilter.selectedOrganizationNode; + } + set selectedOrganizationNode(value: TreeNode) { + this.bridgeService.navigate({ + ...this.routedFilter, + organizationId: value.node.id, + folderId: undefined, + collectionId: undefined, + }); + } + get selectedCipherTypeNode(): TreeNode { + return this.legacyFilter.selectedCipherTypeNode; + } + set selectedCipherTypeNode(value: TreeNode) { + let type: RoutedVaultFilterItemType | undefined; + + if (value?.node.id === "AllItems" && this.routedFilter.organizationIdParamType === "path") { + type = "all"; + } else if ( + value?.node.id === "AllItems" && + this.routedFilter.organizationIdParamType === "query" + ) { + type = undefined; + } else if (isRoutedVaultFilterItemType(value?.node.id)) { + type = value?.node.id; + } + + this.bridgeService.navigate({ + ...this.routedFilter, + type, + folderId: undefined, + collectionId: undefined, + }); + } + get selectedFolderNode(): TreeNode { + return this.legacyFilter.selectedFolderNode; + } + set selectedFolderNode(value: TreeNode) { + const folderId = value != null && value.node.id === null ? Unassigned : value?.node.id; + this.bridgeService.navigate({ + ...this.routedFilter, + folderId, + type: undefined, + collectionId: undefined, + }); + } + get selectedCollectionNode(): TreeNode { + return this.legacyFilter.selectedCollectionNode; + } + set selectedCollectionNode(value: TreeNode) { + let collectionId: string | undefined; + + if (value != null && value.node.id === null) { + collectionId = Unassigned; + } else if ( + value?.node.id === "AllCollections" && + this.routedFilter.organizationIdParamType === "path" + ) { + collectionId = undefined; + } else if ( + value?.node.id === "AllCollections" && + this.routedFilter.organizationIdParamType === "query" + ) { + collectionId = All; + } else { + collectionId = value?.node.id; + } + + this.bridgeService.navigate({ + ...this.routedFilter, + collectionId, + type: undefined, + folderId: undefined, + }); + } + get isFavorites(): boolean { + return this.legacyFilter.isFavorites; + } + get isDeleted(): boolean { + return this.legacyFilter.isDeleted; + } + get organizationId(): string { + return this.legacyFilter.organizationId; + } + get cipherType(): CipherType { + return this.legacyFilter.cipherType; + } + get cipherStatus(): CipherStatus { + return this.legacyFilter.cipherStatus; + } + get cipherTypeId(): string { + return this.legacyFilter.cipherTypeId; + } + get folderId(): string { + return this.legacyFilter.folderId; + } + get collectionId(): string { + return this.legacyFilter.collectionId; + } + resetFilter(): void { + this.bridgeService.navigate({ + ...this.routedFilter, + collectionId: undefined, + folderId: undefined, + organizationId: + this.routedFilter.organizationIdParamType === "path" + ? this.routedFilter.organizationId + : undefined, + type: undefined, + }); + } + resetOrganization(): void { + this.bridgeService.navigate({ ...this.routedFilter, organizationId: undefined }); + } + buildFilter(): VaultFilterFunction { + return this.legacyFilter.buildFilter(); + } +} diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts new file mode 100644 index 0000000000..63a4c4ef0f --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts @@ -0,0 +1,21 @@ +export const Unassigned = "unassigned"; + +export const All = "all"; + +// TODO: Remove `All` when moving to vertical navigation. +const itemTypes = ["favorites", "login", "card", "identity", "note", "trash", All] as const; + +export type RoutedVaultFilterItemType = typeof itemTypes[number]; + +export function isRoutedVaultFilterItemType(value: unknown): value is RoutedVaultFilterItemType { + return itemTypes.includes(value as any); +} + +export interface RoutedVaultFilterModel { + collectionId?: string; + folderId?: string; + organizationId?: string; + type?: RoutedVaultFilterItemType; + + organizationIdParamType?: "path" | "query"; +} diff --git a/apps/web/src/app/vault/individual-vault/vault-items.component.ts b/apps/web/src/app/vault/individual-vault/vault-items.component.ts index 2df96f4c92..185b91e03b 100644 --- a/apps/web/src/app/vault/individual-vault/vault-items.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-items.component.ts @@ -54,8 +54,6 @@ export type VaultItemRow = (CipherView | TreeNode) & { checked templateUrl: "vault-items.component.html", }) export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDestroy { - @Input() showAddNew = true; - @Input() activeFilter: VaultFilter; @Output() activeFilterChanged = new EventEmitter(); @Output() onAttachmentsClicked = new EventEmitter(); @Output() onShareClicked = new EventEmitter(); @@ -63,6 +61,15 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe @Output() onCloneClicked = new EventEmitter(); @Output() onOrganzationBadgeClicked = new EventEmitter(); + private _activeFilter: VaultFilter; + @Input() get activeFilter(): VaultFilter { + return this._activeFilter; + } + set activeFilter(value: VaultFilter) { + this._activeFilter = value; + this.reload(this.activeFilter.buildFilter(), this.activeFilter.isDeleted); + } + cipherType = CipherType; actionPromise: Promise; userHasPremiumAccess = false; @@ -82,6 +89,10 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe protected pagedCollections: TreeNode[] = []; protected searchedCollections: TreeNode[] = []; + get showAddNew() { + return !this.activeFilter.isDeleted; + } + get collections(): TreeNode[] { return this.activeFilter?.selectedCollectionNode?.children ?? []; } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index ceb82cfa38..25f4411875 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -7,7 +7,6 @@ @@ -28,7 +26,6 @@ { + this.activeFilter = activeFilter; + }); } get isShowingCards() { @@ -187,16 +197,6 @@ export class VaultComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - async applyVaultFilter(filter: VaultFilter) { - this.activeFilter = filter; - this.vaultItemsComponent.showAddNew = !this.activeFilter.isDeleted; - await this.vaultItemsComponent.reload( - this.activeFilter.buildFilter(), - this.activeFilter.isDeleted - ); - this.go(); - } - async applyOrganizationFilter(orgId: string) { if (orgId == null) { orgId = "MyVault"; diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts index 2dca1c967a..fa8b6cff5c 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts @@ -1,9 +1,8 @@ import { Component, Input, OnDestroy, OnInit } from "@angular/core"; -import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs"; +import { firstValueFrom, Subject } from "rxjs"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; -import { CollectionView } from "@bitwarden/common/models/view/collection.view"; import { VaultFilterComponent as BaseVaultFilterComponent } from "../../individual-vault/vault-filter/components/vault-filter.component"; //../../vault/vault-filter/components/vault-filter.component"; import { @@ -41,28 +40,6 @@ export class VaultFilterComponent extends BaseVaultFilterComponent implements On this.destroy$.complete(); } - protected loadSubscriptions() { - this.vaultFilterService.filteredCollections$ - .pipe( - switchMap(async (collections) => { - this.removeInvalidCollectionSelection(collections); - }), - takeUntil(this.destroy$) - ) - .subscribe(); - } - - protected async removeInvalidCollectionSelection(collections: CollectionView[]) { - if (this.activeFilter.selectedCollectionNode) { - if (!collections.some((f) => f.id === this.activeFilter.collectionId)) { - this.activeFilter.resetFilter(); - this.activeFilter.selectedCollectionNode = - (await this.getDefaultFilter()) as TreeNode; - this.applyVaultFilter(this.activeFilter); - } - } - } - async buildAllFilters(): Promise { const builderFilter = {} as VaultFilterList; builderFilter.typeFilter = await this.addTypeFilter(["favorites"]); diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index f24d2a4c51..9f6671a8e4 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -10,7 +10,6 @@ #vaultFilter [organization]="organization" [activeFilter]="activeFilter" - (activeFilterChanged)="applyVaultFilter($event)" (onSearchTextChanged)="filterSearchText($event)" > @@ -20,7 +19,6 @@
{ + this.activeFilter = activeFilter; + }); } ngOnDestroy() { @@ -147,16 +157,6 @@ export class VaultComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - async applyVaultFilter(filter: VaultFilter) { - this.activeFilter = filter; - this.vaultItemsComponent.showAddNew = !this.activeFilter.isDeleted; - await this.vaultItemsComponent.reload( - this.activeFilter.buildFilter(), - this.activeFilter.isDeleted - ); - this.go(); - } - async refreshItems() { this.vaultItemsComponent.actionPromise = this.vaultItemsComponent.refresh(); await this.vaultItemsComponent.actionPromise; diff --git a/libs/common/src/misc/serviceUtils.ts b/libs/common/src/misc/serviceUtils.ts index ac85cf0853..eb035a77ba 100644 --- a/libs/common/src/misc/serviceUtils.ts +++ b/libs/common/src/misc/serviceUtils.ts @@ -70,14 +70,14 @@ export class ServiceUtils { /** * Searches a tree for a node with a matching `id` - * @param {TreeNode} nodeTree - A single TreeNode branch that will be searched + * @param {TreeNode} nodeTree - A single TreeNode branch that will be searched * @param {string} id - The id of the node to be found - * @returns {TreeNode} The node with a matching `id` + * @returns {TreeNode} The node with a matching `id` */ - static getTreeNodeObject( - nodeTree: TreeNode, + static getTreeNodeObject( + nodeTree: TreeNode, id: string - ): TreeNode { + ): TreeNode { if (nodeTree.node.id === id) { return nodeTree; }