Skip to content

Commit

Permalink
[EC-14] Refactor vault filter (#3440)
Browse files Browse the repository at this point in the history
* [EC-14] initial refactoring of vault filter

* [EC-14] return observable trees for all filters with head node

* [EC-14] Remove bindings on callbacks

* [EC-14] fix formatting on disabled orgs

* [EC-14] hide MyVault if personal org policy

* [EC-14] add check for single org policy

* [EC-14] add policies to org and change node constructor

* [EC-14] don't show options if personal vault policy

* [EC-14] default to all vaults

* [EC-14] add default selection to filters

* [EC-14] finish filter model callbacks

* [EC-14] finish filter functionality and begin cleaning up

* [EC-14] clean up old components and start on org vault

* [EC-14] loop through filters for presentation

* [EC-14] refactor VaultFilterService and put filter presentation data back into Vault Filter component. Remove VaultService

* [EC-14] begin refactoring org vault

* [EC-14] Refactor Vault Filter Service to use observables

* [EC-14] finish org vault filter

* [EC-14] fix vault model tests

* [EC-14] fix org service calls

* [EC-14] pull refactor out of shared code

* [EC-14] include head node for collections even if collections aren't loaded yet

* [EC-14] fix url params for vaults

* [EC-14] remove comments

* [EC-14] Remove unnecesary getter for org on vault filter

* [EC-14] fix linter

* [EC-14] fix prettier

* [EC-14] add deprecated methods to collection service for desktop and browser

* [EC-14] simplify cipher type node check

* [EC-14] add getters to vault filter model

* [EC-14] refactor how we build the filter list into methods

* [EC-14] add getters to build filter method

* [EC-14] remove param ids if false

* [EC-14] fix collapsing nodes

* [EC-14] add specific type to search placeholder

* [EC-14] remove extra constructor and comment from org vault filter

* [EC-14] extract subscription callback to methods

* [EC-14] Remove unecessary await

* [EC-14] Remove ternary operators while building org filter

* [EC-14] remove unnecessary deps array in vault filter service declaration

* [EC-14] consolidate new models into one file

* [EC-14] initialize nested observable inside of service

Signed-off-by: Jacob Fink <jfink@bitwarden.com>

* [EC-14] change how we load orgs into the vault filter and select the default filter

* [EC-14] remove get from getters name

* [EC-14] remove eslint-disable comment

* [EC-14] move vault filter service abstraction to angular folder and separate

* [EC-14] rename filter types and delete VaultFilterLabel

* [EC-14] remove changes to workspace file

* [EC-14] remove deprecated service from jslib module

* [EC-14] remove any remaining files from common code

* [EC-14] consolidate vault filter components into components folder

* [EC-14] simplify method call

* [EC-14] refactor the vault filter service
- orgs now have observable property
- BehaviorSubjects have been migrated to ReplaySubjects if they don't need starting value
- added unit tests
- fix small error when selecting org badge of personal vault
- renamed some properties

* [EC-14] replace mergeMap with switchMap in vault filter service

* [EC-14] early return to prevent nesting

* [EC-14] clean up filterCollections method

* [EC-14] use isDeleted helper in html

* [EC-14] add jsdoc comments to ServiceUtils

* [EC-14] fix linter

* [EC-14] use array.slice instead of setting length

* Update apps/web/src/app/vault/vault-filter/services/vault-filter.service.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* [EC-14] add missing high level jsdoc description

* [EC-14] fix storybook absolute imports

* [EC-14] delete vault-shared.module

* [EC-14] change search placeholder text to getter and add missing strings

* [EC-14] remove two way binding from search text in vault filter

* [EC-14] removed all binding from search text and just use input event

* [EC-14] remove async from apply vault filter

* [EC-14] remove circular observable calls in vault filter service

Co-authored-by: Thomas Rittson <eliykat@users.noreply.github.com>

* [EC-14] move collapsed nodes to vault filter section

* [EC-14] deconstruct filter section inside component

* [EC-14] fix merge conflicts and introduce refactored organization service to vault filter service

* [EC-14] remove mutation from filter builders

* [EC-14] fix styling on buildFolderTree

* [EC-14] remove leftover folder-filters reference and use ternary for collapse icon

* [EC-14] remove unecessary checks

* [EC-14] stop rebuilding filters when the organization changes

* [EC-14] Move subscription out of setter in vault filter section

* [EC-14] remove extra policy service methods from vault filter service

* [EC-14] remove new methods from old vault-filter.service

* [EC-14] Use vault filter service in vault components

* [EC-14] reload collections from vault now that we have vault filter service

* [EC-14] remove currentFilterCollections in vault filter component

* [EC-14] change VaultFilterType to more specific OrganizationFilter in organization-options

* [EC-14] include org check in isNodeSelected

* [EC-14] add getters to filter function, fix storybook, and add test for All Collections

* [EC-14] show org options even if there's a personal vault policy

* [EC-14] use !"AllCollections" instead of just !null

* [EC-14] Remove extra org Subject in vault filter service

* [EC-14] remove null check from vault search text

* [EC-14] replace store/build names with set/get. Remove extra call to setOrganizationFilter

* [EC-14] add take(1) to subscribe in test

* [EC-14] move init logic in org vault filter component to ngOnInit

* [EC-14] Fix linter

* [EC-14] revert change to vault filter model

* [EC-14] be specific about ignoring All Collections

* [EC-14] move observable init logic to beforeEach in test

* [EC-14] make buildAllFilters return something to reduce side effects

Signed-off-by: Jacob Fink <jfink@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Thomas Rittson <eliykat@users.noreply.github.com>
  • Loading branch information
3 people committed Oct 6, 2022
1 parent dc0ea9a commit 4d83b81
Show file tree
Hide file tree
Showing 57 changed files with 2,172 additions and 998 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";

import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/abstractions/deprecated-vault-filter.service";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";

Expand All @@ -22,6 +23,11 @@ import { VaultFilterComponent } from "./vault-filter.component";
TypeFilterComponent,
],
exports: [VaultFilterComponent],
providers: [VaultFilterService],
providers: [
{
provide: DeprecatedVaultFilterServiceAbstraction,
useClass: VaultFilterService,
},
],
})
export class VaultFilterModule {}
Original file line number Diff line number Diff line change
@@ -1,28 +1,75 @@
import { Component } from "@angular/core";
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";

import { Organization } from "@bitwarden/common/models/domain/organization";
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
import { CollectionView } from "@bitwarden/common/models/view/collectionView";

import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../vault/vault-filter/vault-filter.component";
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../vault/vault-filter/components/vault-filter.component";
import {
VaultFilterList,
VaultFilterType,
} from "../../../vault/vault-filter/shared/models/vault-filter-section.type";
import { CollectionFilter } from "../../../vault/vault-filter/shared/models/vault-filter.type";

@Component({
selector: "app-organization-vault-filter",
templateUrl: "../../../vault/vault-filter/vault-filter.component.html",
templateUrl: "../../../vault/vault-filter/components/vault-filter.component.html",
})
export class VaultFilterComponent extends BaseVaultFilterComponent {
hideOrganizations = true;
hideFavorites = true;
hideFolders = true;
export class VaultFilterComponent extends BaseVaultFilterComponent implements OnInit, OnDestroy {
@Input() set organization(value: Organization) {
if (value && value !== this._organization) {
this._organization = value;
this.vaultFilterService.setOrganizationFilter(this._organization);
}
}
_organization: Organization;
destroy$: Subject<void>;

async ngOnInit() {
this.filters = await this.buildAllFilters();
if (!this.activeFilter.selectedCipherTypeNode) {
this.applyCollectionFilter((await this.getDefaultFilter()) as TreeNode<CollectionFilter>);
}
this.isLoaded = true;
}

organization: Organization;
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}

protected loadSubscriptions() {
this.vaultFilterService.filteredCollections$
.pipe(
switchMap(async (collections) => {
this.removeInvalidCollectionSelection(collections);
}),
takeUntil(this.destroy$)
)
.subscribe();
}

async initCollections() {
if (this.organization.canEditAnyCollection) {
return await this.vaultFilterService.buildAdminCollections(this.organization.id);
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);
}
}
return await this.vaultFilterService.buildCollections(this.organization.id);
}

async reloadCollectionsAndFolders() {
this.collections = await this.initCollections();
async buildAllFilters(): Promise<VaultFilterList> {
const builderFilter = {} as VaultFilterList;
builderFilter.typeFilter = await this.addTypeFilter();
builderFilter.collectionFilter = await this.addCollectionFilter();
builderFilter.trashFilter = await this.addTrashFilter();
return builderFilter;
}

async getDefaultFilter(): Promise<TreeNode<VaultFilterType>> {
return await firstValueFrom(this.filters?.collectionFilter.data$);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { NgModule } from "@angular/core";

import { VaultFilterService as VaultFilterServiceAbstraction } from "../../../vault/vault-filter/services/abstractions/vault-filter.service";
import { VaultFilterSharedModule } from "../../../vault/vault-filter/shared/vault-filter-shared.module";

import { VaultFilterComponent } from "./vault-filter.component";
import { VaultFilterService } from "./vault-filter.service";

@NgModule({
imports: [VaultFilterSharedModule],
declarations: [VaultFilterComponent],
exports: [VaultFilterComponent],
providers: [
{
provide: VaultFilterServiceAbstraction,
useClass: VaultFilterService,
},
],
})
export class VaultFilterModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Injectable } from "@angular/core";
import { combineLatestWith, ReplaySubject, switchMap, takeUntil } from "rxjs";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CollectionData } from "@bitwarden/common/models/data/collectionData";
import { Collection } from "@bitwarden/common/models/domain/collection";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collectionResponse";
import { CollectionView } from "@bitwarden/common/models/view/collectionView";

import { VaultFilterService as BaseVaultFilterService } from "../../../vault/vault-filter/services/vault-filter.service";

@Injectable()
export class VaultFilterService extends BaseVaultFilterService {
protected collectionViews$ = new ReplaySubject<CollectionView[]>(1);

constructor(
stateService: StateService,
organizationService: OrganizationService,
folderService: FolderService,
cipherService: CipherService,
collectionService: CollectionService,
policyService: PolicyService,
protected apiService: ApiService,
i18nService: I18nService
) {
super(
stateService,
organizationService,
folderService,
cipherService,
collectionService,
policyService,
i18nService
);
}

protected loadSubscriptions() {
this.folderService.folderViews$
.pipe(
combineLatestWith(this._organizationFilter),
switchMap(async ([folders, org]) => {
return this.filterFolders(folders, org);
}),
takeUntil(this.destroy$)
)
.subscribe(this._filteredFolders);

this._organizationFilter
.pipe(
switchMap((org) => {
return this.loadCollections(org);
})
)
.subscribe(this.collectionViews$);

this.collectionViews$
.pipe(
combineLatestWith(this._organizationFilter),
switchMap(async ([collections, org]) => {
if (org?.canUseAdminCollections) {
return collections;
} else {
return await this.filterCollections(collections, org);
}
}),
takeUntil(this.destroy$)
)
.subscribe(this._filteredCollections);
}

protected async loadCollections(org: Organization) {
if (org?.permissions && org?.canEditAnyCollection) {
return await this.loadAdminCollections(org);
} else {
// TODO: remove when collections is refactored with observables
return await this.collectionService.getAllDecrypted();
}
}

async loadAdminCollections(org: Organization): Promise<CollectionView[]> {
let collections: CollectionView[] = [];
if (org?.permissions && org?.canEditAnyCollection) {
const collectionResponse = await this.apiService.getCollections(org.id);
if (collectionResponse?.data != null && collectionResponse.data.length) {
const collectionDomains = collectionResponse.data.map(
(r: CollectionDetailsResponse) => new Collection(new CollectionData(r))
);
collections = await this.collectionService.decryptMany(collectionDomains);
}

const noneCollection = new CollectionView();
noneCollection.name = this.i18nService.t("unassigned");
noneCollection.organizationId = org.id;
collections.push(noneCollection);
}
return collections;
}
}
13 changes: 9 additions & 4 deletions apps/web/src/app/organizations/vault/vault.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
<div class="inner-content">
<app-organization-vault-filter
#vaultFilter
[organization]="organization"
[activeFilter]="activeFilter"
(onFilterChange)="applyVaultFilter($event)"
(activeFilterChanged)="applyVaultFilter($event)"
(onSearchTextChanged)="filterSearchText($event)"
></app-organization-vault-filter>
</div>
Expand All @@ -32,21 +33,25 @@ <h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions
[ciphersComponent]="ciphersComponent"
[deleted]="deleted"
[deleted]="activeFilter.isDeleted"
[organization]="organization"
>
</app-vault-bulk-actions>
<button
type="button"
class="btn btn-outline-primary btn-sm ml-auto"
(click)="addCipher()"
*ngIf="!deleted"
*ngIf="!activeFilter.isDeleted"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
</button>
</div>
</div>
<app-callout type="warning" *ngIf="deleted" icon="bwi bwi-exclamation-triangle">
<app-callout
type="warning"
*ngIf="activeFilter.isDeleted"
icon="bwi bwi-exclamation-triangle"
>
{{ trashCleanupWarning }}
</app-callout>
<app-org-vault-ciphers
Expand Down
Loading

0 comments on commit 4d83b81

Please sign in to comment.