Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e8e8f7b
[EC-775] feat: add compatibility layer from #4154
coroiu Feb 3, 2023
f9888b6
[EC-775] fix: ciphers not reloading on filter change
coroiu Feb 3, 2023
9f0dcfe
[EC-775] feat: add support for cipher types
coroiu Feb 6, 2023
096e7fd
[EC-775] feat: implement organization switching
coroiu Feb 7, 2023
fcc49a2
Merge branch 'master' into EC-775-technical-dependency-refactor-vault…
coroiu Feb 13, 2023
b2c341f
[EC-775] feat: remove invalid folder and collection checks
coroiu Feb 14, 2023
7594233
[EC-775] fix: fix reverse data flow race condition
coroiu Feb 14, 2023
19c5bab
[EC-775] fix: No folder use-case not working
coroiu Feb 14, 2023
4eaeb9b
[EC-775] feat: make navigation behave like master
coroiu Feb 14, 2023
4452ddd
[EC-775] feat: add support for trash
coroiu Feb 14, 2023
1e13859
[EC-775] chore: simplify findNode
coroiu Feb 15, 2023
0b6b075
[EC-775] feat: add support for org vault
coroiu Feb 15, 2023
31944d0
[EC-775] feat: add support for orgId in path
coroiu Feb 15, 2023
9e4b5b3
[EC-775] feat: use proper treenode constructor
coroiu Feb 17, 2023
81295c5
[EC-775] chore: remove unnecessary variable
coroiu Feb 17, 2023
a3b0059
[EC-775] docs: add docs to relevant classes
coroiu Feb 17, 2023
bdd3d80
[EC-775] chore: use existing function for searching tree
coroiu Feb 17, 2023
5dd50c0
[EC-775] fix: hide "new" button in trash view
coroiu Feb 17, 2023
ba558d8
[EC-775] feat: add explicit handling for `AllItems`
coroiu Feb 17, 2023
7726d78
[EC-775] fix: prune folderId when changing organization
coroiu Feb 17, 2023
d7d39a6
[EC-775] fix: properly use `undefined` instead of `null`
coroiu Feb 17, 2023
a8e66fc
[EC-775] chore: simplify setters using ternary operator
coroiu Feb 17, 2023
284d64d
[EC-775] feat: add static typing to `type` filter
coroiu Feb 17, 2023
c274133
[EC-775] feat: use new `All` variable for collections
coroiu Feb 20, 2023
7dbe8dc
[EC-775] feat: return `RouterLink` compatible link from `createRoute`
coroiu Feb 20, 2023
3a996e9
[EC-775] feat: add ordId path support to `createRoute`
coroiu Feb 20, 2023
a69b064
[EC-775] fix: interpret params differently in org vault
coroiu Feb 20, 2023
61395dc
[EC-775] doc: clarify `createRoute`
coroiu Feb 20, 2023
2529205
[EC-775] fix: better `type` typing
coroiu Feb 20, 2023
10a8b32
[EC-775] feat: remove support for path navigation
coroiu Feb 21, 2023
e1803bf
[EC-775] fix: refactor bridge service to improve readability
coroiu Feb 21, 2023
c20cc6f
Merge branch 'master' into EC-775-technical-dependency-refactor-vault…
coroiu Feb 21, 2023
319d469
Merge branch 'master' into EC-775-technical-dependency-refactor-vault…
coroiu Feb 23, 2023
72de73d
Merge branch 'master' into EC-775-technical-dependency-refactor-vault…
coroiu Mar 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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>();
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand All @@ -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> => {
Expand All @@ -185,30 +153,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
return await firstValueFrom(this.filters?.typeFilter.data$);
}

protected async removeInvalidFolderSelection(folders: FolderView[]) {
Comment thread
coroiu marked this conversation as resolved.
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { combineLatest, map, Observable } from "rxjs";

import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/tree-node";

import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model";
import { RoutedVaultFilterModel, Unassigned } from "../shared/models/routed-vault-filter.model";
import { VaultFilter } from "../shared/models/vault-filter.model";
import { CipherTypeFilter } from "../shared/models/vault-filter.type";

import { VaultFilterService } from "./abstractions/vault-filter.service";
import { RoutedVaultFilterService } from "./routed-vault-filter.service";

@Injectable()
export class RoutedVaultFilterBridgeService {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: you could add a jsdoc comment here explaining the purpose of the bridge service

@eliykat eliykat Feb 17, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I'm wondering about 😁 is it transitional? If so, do we need a tech debt ticket to remove the old services? I assume that when we build the new filter list in the left-hand nav, they'll be routing directly to the query URL rather than going via a service, and then it'll be much simpler to clean up the old code. This feels like something EC pod should do as part of VVR, before handing off to Vault team.

Note that we also have DeprecatedVaultFilterService floating around 🤯

EDIT: ok, after reviewing the rest of the code, (I think) I understand that this is basically a wrapper around existing logic to implement routing with a minimum of code changes. The downside is that it adds complexity in the short term, but I'm OK with this if we're deprecating it quickly after VVR (because the new components won't use it).

@coroiu coroiu Feb 17, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jlf0dev Sure I can add the file descriptions I've used in the PR description field into the code!

@eliykat Yes exactly, this is a transitional class. I'm glad that it becomes apparent when reading the code, means it's not too complex/confusing. As I mention in the Objective part of the PR description:

This PR introduces an temporary bridge between URL filtering and the old state-in-code method. It enables us to move forward with refactoring the table and the navigation, and allow us to work on them separately.

You have a good point, maybe we should add a TD ticket, but really all of this should be removed as part of VVR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a TD ticket I added a task to the VVR story instead EC-208 [Admin Console] Add Filters under Left Nav

@eliykat eliykat Feb 20, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, and serves me right for not reading your PR description properly 🙈
I'll let @jlf0dev mark this thread resolved once he's had a chance to look at it

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 = new VaultFilter();

if (filter.collectionId !== undefined && filter.collectionId === Unassigned) {
legacyFilter.selectedCollectionNode = this.findNode(collectionTree, null);
}

if (filter.collectionId !== undefined && filter.collectionId !== Unassigned) {
legacyFilter.selectedCollectionNode = this.findNode(collectionTree, filter.collectionId);
}

if (filter.folderId !== undefined && filter.folderId === Unassigned) {
legacyFilter.selectedFolderNode = this.findNode(folderTree, null);
}

if (filter.folderId !== undefined && filter.folderId !== Unassigned) {
legacyFilter.selectedFolderNode = this.findNode(folderTree, filter.folderId);
}

if (filter.organizationId !== undefined) {
legacyFilter.selectedOrganizationNode = this.findNode(
organizationTree,
filter.organizationId
);
}

if (filter.type !== undefined && filter.type === "trash") {
legacyFilter.selectedCipherTypeNode = {
node: { id: "trash", name: "", icon: "", type: "trash" },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor suggestion: you could use the TreeNode constructor here:

legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>({ id: "trash", name: "", type: "trash", icon: ""}, null);

we really should refactor this constructor to be more useful, I don't even think this will save you that many characters 😄

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question was going to be whether we could find the existing Trash node, or export & import the Trash node object, rather than recreating it. It looks like it's currently declared in the VaultFilterComponent though, which I guess we don't really want to pull in to a service.

In any case I agree you should use a real TreeNode object, not really to save any characters, just to be properly typesafe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jlf0dev Good point, will update!

@eliykat Yeah that was exactly the issue, I contemplated moving it from VaultFilterComponent to VaultFilterService but I really wanted to keep the modifications to a minimum, so this is what I ended up with it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm but I'll let @jlf0dev resolve the thread when he's happy

} as unknown as TreeNode<CipherTypeFilter>;
}

if (filter.type !== undefined && filter.type !== "trash") {
legacyFilter.selectedCipherTypeNode = this.findNode(cipherTypeTree, filter.type);
}

const bridgeModel = new RoutedVaultFilterBridge(filter, legacyFilter, this);

return bridgeModel;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: unnecessary variable here, just return the model

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I wonder why I did this. Must've been a console.log here at some point 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

})
);
}

navigate(filter: RoutedVaultFilterModel) {
const route = this.routedVaultFilterService.createRoute(filter);
this.router.navigate(route.commands, route.extras);
}

private findNode<T extends ITreeNodeObject>(
Comment thread
eliykat marked this conversation as resolved.
Outdated
node: TreeNode<T>,
id: string
): TreeNode<T> | undefined {
if (node.node.id === id) {

@jlf0dev jlf0dev Feb 16, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: we need to rename the node to object or something at some point 😄

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably some background discussion here that I'm not familiar with, but - object seems worse! Everything's an object! 😅 and Object is a keyword.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha yeah, hopefully we'll get rid of the TreeNode completely and not have to worry about it :D

return node;
}

for (const child of node.children) {
const result = this.findNode(child, id);
if (result !== undefined) {
return result;
}
}

return undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Injectable, OnDestroy } from "@angular/core";
import { ActivatedRoute, NavigationExtras } from "@angular/router";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";

import { RoutedVaultFilterModel } from "../shared/models/routed-vault-filter.model";

@Injectable()
export class RoutedVaultFilterService implements OnDestroy {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: a comment here might be helpful as well. The name RoutedVaultFilterService makes sense, but only if you understand why we need it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

private onDestroy = new Subject<void>();

filter$: Observable<RoutedVaultFilterModel>;

constructor(activatedRoute: ActivatedRoute) {
this.filter$ = combineLatest([activatedRoute.paramMap, activatedRoute.queryParamMap]).pipe(
map(([params, queryParams]) => {
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: queryParams.get("type") ?? undefined,
};
}),
takeUntil(this.onDestroy)
);
}

createRoute(filter: RoutedVaultFilterModel): { commands: any[]; extras?: NavigationExtras } {
return {
commands: [],
extras: {
queryParams: {
collectionId: filter.collectionId ?? null,
Comment thread
eliykat marked this conversation as resolved.
Outdated
folderId: filter.folderId ?? null,
organizationId:
filter.organizationIdParamType === "path" ? null : filter.organizationId ?? null,
type: filter.type ?? null,
},
queryParamsHandling: "merge",
},
};
}

ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 "../../../../app/organizations/core";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -253,4 +256,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>> {
Comment thread
eliykat marked this conversation as resolved.
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
);
}
}
Loading