mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-01 23:01:28 +01:00
[EC-775] [Technical Dependency] Refactor Vault Filters to be routable (#4733)
* [EC-775] feat: add compatibility layer from #4154 * [EC-775] fix: ciphers not reloading on filter change * [EC-775] feat: add support for cipher types * [EC-775] feat: implement organization switching * [EC-775] feat: remove invalid folder and collection checks Had to remove these becuase they were causing double navigations on each click. * [EC-775] fix: fix reverse data flow race condition vault-filter.component was pushing up old filter models which would sometimes overwrite new filter models that came from the routed filter service. * [EC-775] fix: No folder use-case not working * [EC-775] feat: make navigation behave like master * [EC-775] feat: add support for trash * [EC-775] chore: simplify findNode * [EC-775] feat: add support for org vault * [EC-775] feat: add support for orgId in path * [EC-775] feat: use proper treenode constructor * [EC-775] chore: remove unnecessary variable * [EC-775] docs: add docs to relevant classes * [EC-775] chore: use existing function for searching tree * [EC-775] fix: hide "new" button in trash view * [EC-775] feat: add explicit handling for `AllItems` * [EC-775] fix: prune folderId when changing organization * [EC-775] fix: properly use `undefined` instead of `null` * [EC-775] chore: simplify setters using ternary operator * [EC-775] feat: add static typing to `type` filter * [EC-775] feat: use new `All` variable for collections * [EC-775] feat: return `RouterLink` compatible link from `createRoute` * [EC-775] feat: add ordId path support to `createRoute` * [EC-775] fix: interpret params differently in org vault This is needed due to how defaults used to work when using `state-in-code`. We really want to get rid of this type of logic going forward. * [EC-775] doc: clarify `createRoute` * [EC-775] fix: better `type` typing * [EC-775] feat: remove support for path navigation It's better that we circle back to this type of navigationt when we're working on the VVR and have more knowledge about how this is supposed to work. * [EC-775] fix: refactor bridge service to improve readability Refactor follows feedback from PR review
This commit is contained in:
parent
ae271b5c16
commit
ea6666780a
@ -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<VaultFilter>();
|
||||
@Output() onSearchTextChanged = new EventEmitter<string>();
|
||||
@Output() onAddFolder = new EventEmitter<never>();
|
||||
@Output() onEditFolder = new EventEmitter<FolderFilter>();
|
||||
@ -88,9 +85,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
protected policyService: PolicyService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService
|
||||
) {
|
||||
this.loadSubscriptions();
|
||||
}
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
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<OrganizationFilter>): Promise<void> => {
|
||||
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<CipherTypeFilter>): Promise<void> => {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedCipherTypeNode = filterNode;
|
||||
this.applyVaultFilter(filter);
|
||||
};
|
||||
|
||||
applyFolderFilter = async (folderNode: TreeNode<FolderFilter>): Promise<void> => {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedFolderNode = folderNode;
|
||||
this.applyVaultFilter(filter);
|
||||
};
|
||||
|
||||
applyCollectionFilter = async (collectionNode: TreeNode<CollectionFilter>): Promise<void> => {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedCollectionNode = collectionNode;
|
||||
this.applyVaultFilter(filter);
|
||||
};
|
||||
|
||||
addFolder = async (): Promise<void> => {
|
||||
@ -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<CipherTypeFilter>;
|
||||
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<CipherTypeFilter>;
|
||||
this.applyVaultFilter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||
|
@ -19,6 +19,7 @@ export abstract class VaultFilterService {
|
||||
organizationTree$: Observable<TreeNode<OrganizationFilter>>;
|
||||
folderTree$: Observable<TreeNode<FolderFilter>>;
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>>;
|
||||
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>>;
|
||||
reloadCollections: () => Promise<void>;
|
||||
getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
|
||||
setCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
|
||||
|
@ -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<VaultFilter>;
|
||||
|
||||
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<CollectionFilter>,
|
||||
cipherTypeTree: TreeNode<CipherTypeFilter>
|
||||
): 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<CipherTypeFilter>(
|
||||
{ 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<CollectionFilter>,
|
||||
folderTree: TreeNode<FolderFilter>,
|
||||
organizationTree: TreeNode<OrganizationFilter>,
|
||||
cipherTypeTree: TreeNode<CipherTypeFilter>
|
||||
): 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<CipherTypeFilter>(
|
||||
{ id: "trash", name: "", type: "trash", icon: "" },
|
||||
null
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type !== "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
filter.type
|
||||
);
|
||||
}
|
||||
|
||||
return legacyFilter;
|
||||
}
|
@ -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<void>();
|
||||
|
||||
/**
|
||||
* Filter values extracted from the URL.
|
||||
* To change the values use {@link RoutedVaultFilterService.createRoute}.
|
||||
*/
|
||||
filter$: Observable<RoutedVaultFilterModel>;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -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<TreeNode<CipherTypeFilter>> = 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<FolderFilter>(head, null, "folders", "AllFolders");
|
||||
}
|
||||
|
||||
protected buildCipherTypeTree(): Observable<TreeNode<CipherTypeFilter>> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<CollectionFilter>[] {
|
||||
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<OrganizationFilter> {
|
||||
return this.legacyFilter.selectedOrganizationNode;
|
||||
}
|
||||
set selectedOrganizationNode(value: TreeNode<OrganizationFilter>) {
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
organizationId: value.node.id,
|
||||
folderId: undefined,
|
||||
collectionId: undefined,
|
||||
});
|
||||
}
|
||||
get selectedCipherTypeNode(): TreeNode<CipherTypeFilter> {
|
||||
return this.legacyFilter.selectedCipherTypeNode;
|
||||
}
|
||||
set selectedCipherTypeNode(value: TreeNode<CipherTypeFilter>) {
|
||||
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<FolderFilter> {
|
||||
return this.legacyFilter.selectedFolderNode;
|
||||
}
|
||||
set selectedFolderNode(value: TreeNode<FolderFilter>) {
|
||||
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<CollectionFilter> {
|
||||
return this.legacyFilter.selectedCollectionNode;
|
||||
}
|
||||
set selectedCollectionNode(value: TreeNode<CollectionFilter>) {
|
||||
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();
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -54,8 +54,6 @@ export type VaultItemRow = (CipherView | TreeNode<CollectionFilter>) & { checked
|
||||
templateUrl: "vault-items.component.html",
|
||||
})
|
||||
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDestroy {
|
||||
@Input() showAddNew = true;
|
||||
@Input() activeFilter: VaultFilter;
|
||||
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
|
||||
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
|
||||
@Output() onShareClicked = new EventEmitter<CipherView>();
|
||||
@ -63,6 +61,15 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe
|
||||
@Output() onCloneClicked = new EventEmitter<CipherView>();
|
||||
@Output() onOrganzationBadgeClicked = new EventEmitter<string>();
|
||||
|
||||
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<any>;
|
||||
userHasPremiumAccess = false;
|
||||
@ -82,6 +89,10 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe
|
||||
protected pagedCollections: TreeNode<CollectionFilter>[] = [];
|
||||
protected searchedCollections: TreeNode<CollectionFilter>[] = [];
|
||||
|
||||
get showAddNew() {
|
||||
return !this.activeFilter.isDeleted;
|
||||
}
|
||||
|
||||
get collections(): TreeNode<CollectionFilter>[] {
|
||||
return this.activeFilter?.selectedCollectionNode?.children ?? [];
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
<app-vault-filter
|
||||
#vaultFilter
|
||||
[activeFilter]="activeFilter"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event)"
|
||||
@ -19,7 +18,6 @@
|
||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||
<app-vault-header
|
||||
[activeFilter]="activeFilter"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
[actionPromise]="vaultItemsComponent.actionPromise"
|
||||
(onAddCipher)="addCipher()"
|
||||
></app-vault-header>
|
||||
@ -28,7 +26,6 @@
|
||||
</app-callout>
|
||||
<app-vault-items
|
||||
[activeFilter]="activeFilter"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onCipherClicked)="navigateToCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)"
|
||||
(onAddCipher)="addCipher()"
|
||||
|
@ -37,6 +37,8 @@ import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||
import { RoutedVaultFilterBridgeService } from "./vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "./vault-filter/services/routed-vault-filter.service";
|
||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||
import { VaultItemsComponent } from "./vault-items.component";
|
||||
@ -46,6 +48,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
@Component({
|
||||
selector: "app-vault",
|
||||
templateUrl: "vault.component.html",
|
||||
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
||||
@ -88,6 +91,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService,
|
||||
private cipherService: CipherService,
|
||||
private passwordRepromptService: PasswordRepromptService
|
||||
) {}
|
||||
@ -165,6 +169,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((activeFilter) => {
|
||||
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";
|
||||
|
@ -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<CollectionFilter>;
|
||||
this.applyVaultFilter(this.activeFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.typeFilter = await this.addTypeFilter(["favorites"]);
|
||||
|
@ -10,7 +10,6 @@
|
||||
#vaultFilter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
@ -20,7 +19,6 @@
|
||||
<div class="col-9">
|
||||
<app-org-vault-header
|
||||
[activeFilter]="activeFilter"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onCollectionChanged)="refreshItems()"
|
||||
[actionPromise]="vaultItemsComponent.actionPromise"
|
||||
(onAddCipher)="addCipher()"
|
||||
@ -36,7 +34,6 @@
|
||||
<app-org-vault-items
|
||||
[activeFilter]="activeFilter"
|
||||
[initOrganization]="organization"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onCipherClicked)="navigateToCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)"
|
||||
(onAddCipher)="addCipher()"
|
||||
|
@ -28,6 +28,8 @@ import { EntityEventsComponent } from "../../organizations/manage/entity-events.
|
||||
import { CollectionsComponent } from "../../organizations/vault/collections.component";
|
||||
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
||||
import { RoutedVaultFilterBridgeService } from "../individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { AttachmentsComponent } from "./attachments.component";
|
||||
@ -39,6 +41,7 @@ const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||
@Component({
|
||||
selector: "app-org-vault",
|
||||
templateUrl: "vault.component.html",
|
||||
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("vaultFilter", { static: true })
|
||||
@ -62,6 +65,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
protected vaultFilterService: VaultFilterService,
|
||||
private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService,
|
||||
private router: Router,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private syncService: SyncService,
|
||||
@ -139,6 +143,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
await this.syncService.fullSync(false);
|
||||
}
|
||||
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((activeFilter) => {
|
||||
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;
|
||||
|
@ -70,14 +70,14 @@ export class ServiceUtils {
|
||||
|
||||
/**
|
||||
* Searches a tree for a node with a matching `id`
|
||||
* @param {TreeNode<ITreeNodeObject>} nodeTree - A single TreeNode branch that will be searched
|
||||
* @param {TreeNode<T>} nodeTree - A single TreeNode branch that will be searched
|
||||
* @param {string} id - The id of the node to be found
|
||||
* @returns {TreeNode<ITreeNodeObject>} The node with a matching `id`
|
||||
* @returns {TreeNode<T>} The node with a matching `id`
|
||||
*/
|
||||
static getTreeNodeObject(
|
||||
nodeTree: TreeNode<ITreeNodeObject>,
|
||||
static getTreeNodeObject<T extends ITreeNodeObject>(
|
||||
nodeTree: TreeNode<T>,
|
||||
id: string
|
||||
): TreeNode<ITreeNodeObject> {
|
||||
): TreeNode<T> {
|
||||
if (nodeTree.node.id === id) {
|
||||
return nodeTree;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user