[EC-14] Refactor vault filter (#3440)
* [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>
This commit is contained in:
parent
dc0ea9a48f
commit
4d83b81d82
|
@ -1,6 +1,7 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { BrowserModule } from "@angular/platform-browser";
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||||
|
|
||||||
|
@ -22,6 +23,11 @@ import { VaultFilterComponent } from "./vault-filter.component";
|
||||||
TypeFilterComponent,
|
TypeFilterComponent,
|
||||||
],
|
],
|
||||||
exports: [VaultFilterComponent],
|
exports: [VaultFilterComponent],
|
||||||
providers: [VaultFilterService],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DeprecatedVaultFilterServiceAbstraction,
|
||||||
|
useClass: VaultFilterService,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class VaultFilterModule {}
|
export class VaultFilterModule {}
|
||||||
|
|
|
@ -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 { 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({
|
@Component({
|
||||||
selector: "app-organization-vault-filter",
|
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 {
|
export class VaultFilterComponent extends BaseVaultFilterComponent implements OnInit, OnDestroy {
|
||||||
hideOrganizations = true;
|
@Input() set organization(value: Organization) {
|
||||||
hideFavorites = true;
|
if (value && value !== this._organization) {
|
||||||
hideFolders = true;
|
this._organization = value;
|
||||||
|
this.vaultFilterService.setOrganizationFilter(this._organization);
|
||||||
organization: Organization;
|
|
||||||
|
|
||||||
async initCollections() {
|
|
||||||
if (this.organization.canEditAnyCollection) {
|
|
||||||
return await this.vaultFilterService.buildAdminCollections(this.organization.id);
|
|
||||||
}
|
}
|
||||||
return await this.vaultFilterService.buildCollections(this.organization.id);
|
}
|
||||||
|
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadCollectionsAndFolders() {
|
ngOnDestroy() {
|
||||||
this.collections = await this.initCollections();
|
this.destroy$.next();
|
||||||
|
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();
|
||||||
|
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||||
|
builderFilter.trashFilter = await this.addTrashFilter();
|
||||||
|
return builderFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefaultFilter(): Promise<TreeNode<VaultFilterType>> {
|
||||||
|
return await firstValueFrom(this.filters?.collectionFilter.data$);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
import { NgModule } from "@angular/core";
|
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 { VaultFilterSharedModule } from "../../../vault/vault-filter/shared/vault-filter-shared.module";
|
||||||
|
|
||||||
import { VaultFilterComponent } from "./vault-filter.component";
|
import { VaultFilterComponent } from "./vault-filter.component";
|
||||||
|
import { VaultFilterService } from "./vault-filter.service";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [VaultFilterSharedModule],
|
imports: [VaultFilterSharedModule],
|
||||||
declarations: [VaultFilterComponent],
|
declarations: [VaultFilterComponent],
|
||||||
exports: [VaultFilterComponent],
|
exports: [VaultFilterComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VaultFilterServiceAbstraction,
|
||||||
|
useClass: VaultFilterService,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class VaultFilterModule {}
|
export class VaultFilterModule {}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,8 +6,9 @@
|
||||||
<div class="inner-content">
|
<div class="inner-content">
|
||||||
<app-organization-vault-filter
|
<app-organization-vault-filter
|
||||||
#vaultFilter
|
#vaultFilter
|
||||||
|
[organization]="organization"
|
||||||
[activeFilter]="activeFilter"
|
[activeFilter]="activeFilter"
|
||||||
(onFilterChange)="applyVaultFilter($event)"
|
(activeFilterChanged)="applyVaultFilter($event)"
|
||||||
(onSearchTextChanged)="filterSearchText($event)"
|
(onSearchTextChanged)="filterSearchText($event)"
|
||||||
></app-organization-vault-filter>
|
></app-organization-vault-filter>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<app-vault-bulk-actions
|
<app-vault-bulk-actions
|
||||||
[ciphersComponent]="ciphersComponent"
|
[ciphersComponent]="ciphersComponent"
|
||||||
[deleted]="deleted"
|
[deleted]="activeFilter.isDeleted"
|
||||||
[organization]="organization"
|
[organization]="organization"
|
||||||
>
|
>
|
||||||
</app-vault-bulk-actions>
|
</app-vault-bulk-actions>
|
||||||
|
@ -40,13 +41,17 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-primary btn-sm ml-auto"
|
class="btn btn-outline-primary btn-sm ml-auto"
|
||||||
(click)="addCipher()"
|
(click)="addCipher()"
|
||||||
*ngIf="!deleted"
|
*ngIf="!activeFilter.isDeleted"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 }}
|
{{ trashCleanupWarning }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<app-org-vault-ciphers
|
<app-org-vault-ciphers
|
||||||
|
|
|
@ -8,10 +8,10 @@ import {
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
|
||||||
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
@ -20,11 +20,11 @@ import { OrganizationService } from "@bitwarden/common/abstractions/organization
|
||||||
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
|
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
|
||||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||||
|
|
||||||
import { VaultService } from "../../vault/shared/vault.service";
|
import { VaultFilterService } from "../../vault/vault-filter/services/abstractions/vault-filter.service";
|
||||||
|
import { VaultFilter } from "../../vault/vault-filter/shared/models/vault-filter.model";
|
||||||
import { EntityEventsComponent } from "../manage/entity-events.component";
|
import { EntityEventsComponent } from "../manage/entity-events.component";
|
||||||
|
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { AddEditComponent } from "./add-edit.component";
|
||||||
|
@ -53,19 +53,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
eventsModalRef: ViewContainerRef;
|
eventsModalRef: ViewContainerRef;
|
||||||
|
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
collectionId: string = null;
|
|
||||||
type: CipherType = null;
|
|
||||||
trashCleanupWarning: string = null;
|
trashCleanupWarning: string = null;
|
||||||
activeFilter: VaultFilter = new VaultFilter();
|
activeFilter: VaultFilter = new VaultFilter();
|
||||||
|
|
||||||
// This is a hack to avoid redundant api calls that fetch OrganizationVaultFilterComponent collections
|
|
||||||
// When it makes sense to do so we should leverage some other communication method for change events that isn't directly tied to the query param for organizationId
|
|
||||||
// i.e. exposing the VaultFiltersService to the OrganizationSwitcherComponent to make relevant updates from a change event instead of just depending on the router
|
|
||||||
firstLoaded = true;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private vaultFilterService: VaultFilterService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
|
@ -75,7 +69,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private vaultService: VaultService,
|
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private passwordRepromptService: PasswordRepromptService
|
private passwordRepromptService: PasswordRepromptService
|
||||||
) {}
|
) {}
|
||||||
|
@ -88,8 +81,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.params.subscribe(async (params: any) => {
|
this.route.parent.params.subscribe(async (params: any) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = this.organizationService.get(params.organizationId);
|
||||||
this.vaultFilterComponent.organization = this.organization;
|
|
||||||
this.ciphersComponent.organization = this.organization;
|
this.ciphersComponent.organization = this.organization;
|
||||||
|
|
||||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||||
|
@ -103,7 +95,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
case "syncCompleted":
|
case "syncCompleted":
|
||||||
if (message.successfully) {
|
if (message.successfully) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.vaultFilterComponent.reloadCollectionsAndFolders(),
|
this.vaultFilterService.reloadCollections(),
|
||||||
this.ciphersComponent.refresh(),
|
this.ciphersComponent.refresh(),
|
||||||
]);
|
]);
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
|
@ -114,12 +106,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.firstLoaded) {
|
await this.ciphersComponent.reload(
|
||||||
await this.vaultFilterComponent.reloadCollectionsAndFolders();
|
this.activeFilter.buildFilter(),
|
||||||
}
|
this.activeFilter.isDeleted
|
||||||
this.firstLoaded = true;
|
);
|
||||||
|
|
||||||
await this.ciphersComponent.reload();
|
|
||||||
|
|
||||||
if (qParams.viewEvents != null) {
|
if (qParams.viewEvents != null) {
|
||||||
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
|
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
|
||||||
|
@ -134,7 +124,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
if (cipherId) {
|
if (cipherId) {
|
||||||
if (
|
if (
|
||||||
// Handle users with implicit collection access since they use the admin endpoint
|
// Handle users with implicit collection access since they use the admin endpoint
|
||||||
this.organization.canEditAnyCollection ||
|
this.organization.canUseAdminCollections ||
|
||||||
(await this.cipherService.get(cipherId)) != null
|
(await this.cipherService.get(cipherId)) != null
|
||||||
) {
|
) {
|
||||||
this.editCipherId(cipherId);
|
this.editCipherId(cipherId);
|
||||||
|
@ -155,23 +145,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get deleted(): boolean {
|
|
||||||
return this.activeFilter.status === "trash";
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
async applyVaultFilter(filter: VaultFilter) {
|
||||||
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
this.activeFilter = filter;
|
||||||
this.activeFilter = vaultFilter;
|
this.ciphersComponent.showAddNew = !this.activeFilter.isDeleted;
|
||||||
await this.ciphersComponent.reload(
|
await this.ciphersComponent.reload(
|
||||||
this.activeFilter.buildFilter(),
|
this.activeFilter.buildFilter(),
|
||||||
vaultFilter.status === "trash"
|
this.activeFilter.isDeleted
|
||||||
);
|
);
|
||||||
this.vaultFilterComponent.searchPlaceholder =
|
|
||||||
this.vaultService.calculateSearchBarLocalizationString(this.activeFilter);
|
|
||||||
this.go();
|
this.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,16 +195,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipherCollections(cipher: CipherView) {
|
async editCipherCollections(cipher: CipherView) {
|
||||||
|
const currCollections = await firstValueFrom(this.vaultFilterService.filteredCollections$);
|
||||||
const [modal] = await this.modalService.openViewRef(
|
const [modal] = await this.modalService.openViewRef(
|
||||||
CollectionsComponent,
|
CollectionsComponent,
|
||||||
this.collectionsModalRef,
|
this.collectionsModalRef,
|
||||||
(comp) => {
|
(comp) => {
|
||||||
if (this.organization.canEditAnyCollection) {
|
comp.collectionIds = cipher.collectionIds;
|
||||||
comp.collectionIds = cipher.collectionIds;
|
comp.collections = currCollections.filter((c) => !c.readOnly && c.id != null);
|
||||||
comp.collections = this.vaultFilterComponent.collections.fullList.filter(
|
|
||||||
(c) => !c.readOnly && c.id != null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
comp.organization = this.organization;
|
comp.organization = this.organization;
|
||||||
comp.cipherId = cipher.id;
|
comp.cipherId = cipher.id;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
|
@ -235,14 +216,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
async addCipher() {
|
async addCipher() {
|
||||||
const component = await this.editCipher(null);
|
const component = await this.editCipher(null);
|
||||||
component.organizationId = this.organization.id;
|
component.organizationId = this.organization.id;
|
||||||
component.type = this.type;
|
component.type = this.activeFilter.cipherType;
|
||||||
if (this.organization.canEditAnyCollection) {
|
component.collections = (
|
||||||
component.collections = this.vaultFilterComponent.collections.fullList.filter(
|
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||||
(c) => !c.readOnly && c.id != null
|
).filter((c) => !c.readOnly && c.id != null);
|
||||||
);
|
if (this.activeFilter.collectionId) {
|
||||||
}
|
component.collectionIds = [this.activeFilter.collectionId];
|
||||||
if (this.collectionId != null) {
|
|
||||||
component.collectionIds = [this.collectionId];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,13 +273,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
const component = await this.editCipher(cipher);
|
const component = await this.editCipher(cipher);
|
||||||
component.cloneMode = true;
|
component.cloneMode = true;
|
||||||
component.organizationId = this.organization.id;
|
component.organizationId = this.organization.id;
|
||||||
if (this.organization.canEditAnyCollection) {
|
component.collections = (
|
||||||
component.collections = this.vaultFilterComponent.collections.fullList.filter(
|
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||||
(c) => !c.readOnly && c.id != null
|
).filter((c) => !c.readOnly && c.id != null);
|
||||||
);
|
|
||||||
}
|
|
||||||
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
|
|
||||||
// in the add-edit componenet
|
|
||||||
component.collectionIds = cipher.collectionIds;
|
component.collectionIds = cipher.collectionIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,8 +293,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
if (queryParams == null) {
|
if (queryParams == null) {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
type: this.activeFilter.cipherType,
|
type: this.activeFilter.cipherType,
|
||||||
collectionId: this.activeFilter.selectedCollectionId,
|
collectionId: this.activeFilter.collectionId,
|
||||||
deleted: this.deleted ? true : null,
|
deleted: this.activeFilter.isDeleted || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { VaultSharedModule } from "../../vault/shared/vault-shared.module";
|
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
||||||
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
|
|
||||||
import { CiphersComponent } from "./ciphers.component";
|
import { CiphersComponent } from "./ciphers.component";
|
||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
|
@ -8,7 +9,7 @@ import { VaultRoutingModule } from "./vault-routing.module";
|
||||||
import { VaultComponent } from "./vault.component";
|
import { VaultComponent } from "./vault.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [VaultSharedModule, VaultRoutingModule, VaultFilterModule],
|
imports: [VaultRoutingModule, VaultFilterModule, SharedModule, LooseComponentsModule],
|
||||||
declarations: [VaultComponent, CiphersComponent],
|
declarations: [VaultComponent, CiphersComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
})
|
})
|
||||||
|
|
|
@ -126,20 +126,13 @@ import { CollectionsComponent } from "../vault/collections.component";
|
||||||
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
|
||||||
import { OrganizationBadgeModule } from "../vault/organization-badge/organization-badge.module";
|
import { OrganizationBadgeModule } from "../vault/organization-badge/organization-badge.module";
|
||||||
import { ShareComponent } from "../vault/share.component";
|
import { ShareComponent } from "../vault/share.component";
|
||||||
import { VaultFilterModule } from "../vault/vault-filter/vault-filter.module";
|
|
||||||
|
|
||||||
import { SharedModule } from ".";
|
import { SharedModule } from ".";
|
||||||
|
|
||||||
// Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left.
|
// Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left.
|
||||||
// If you are building new functionality, please create or extend a feature module instead.
|
// If you are building new functionality, please create or extend a feature module instead.
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [SharedModule, OrganizationBadgeModule, OrganizationCreateModule, RegisterFormModule],
|
||||||
SharedModule,
|
|
||||||
VaultFilterModule,
|
|
||||||
OrganizationBadgeModule,
|
|
||||||
OrganizationCreateModule,
|
|
||||||
RegisterFormModule,
|
|
||||||
],
|
|
||||||
declarations: [
|
declarations: [
|
||||||
PremiumBadgeComponent,
|
PremiumBadgeComponent,
|
||||||
AcceptEmergencyComponent,
|
AcceptEmergencyComponent,
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { NgModule } from "@angular/core";
|
|
||||||
|
|
||||||
import { SharedModule } from "../../shared";
|
|
||||||
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
|
||||||
import { VaultFilterModule } from "../vault-filter/vault-filter.module";
|
|
||||||
|
|
||||||
import { PipesModule } from "./pipes/pipes.module";
|
|
||||||
import { VaultService } from "./vault.service";
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [SharedModule, VaultFilterModule, LooseComponentsModule, PipesModule],
|
|
||||||
exports: [SharedModule, VaultFilterModule, LooseComponentsModule, PipesModule],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: VaultService,
|
|
||||||
useClass: VaultService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class VaultSharedModule {}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
|
||||||
|
|
||||||
export class VaultService {
|
|
||||||
calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
|
||||||
if (vaultFilter.status === "favorites") {
|
|
||||||
return "searchFavorites";
|
|
||||||
}
|
|
||||||
if (vaultFilter.status === "trash") {
|
|
||||||
return "searchTrash";
|
|
||||||
}
|
|
||||||
if (vaultFilter.cipherType != null) {
|
|
||||||
return "searchType";
|
|
||||||
}
|
|
||||||
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId != "none") {
|
|
||||||
return "searchFolder";
|
|
||||||
}
|
|
||||||
if (vaultFilter.selectedCollection) {
|
|
||||||
return "searchCollection";
|
|
||||||
}
|
|
||||||
if (vaultFilter.selectedOrganizationId != null) {
|
|
||||||
return "searchOrganization";
|
|
||||||
}
|
|
||||||
if (vaultFilter.myVaultOnly) {
|
|
||||||
return "searchMyVault";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "searchVault";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
@ -14,6 +14,8 @@ import { Policy } from "@bitwarden/common/models/domain/policy";
|
||||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/models/request/organizationUserResetPasswordEnrollmentRequest";
|
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/models/request/organizationUserResetPasswordEnrollmentRequest";
|
||||||
|
|
||||||
import { EnrollMasterPasswordReset } from "../../../organizations/users/enroll-master-password-reset.component";
|
import { EnrollMasterPasswordReset } from "../../../organizations/users/enroll-master-password-reset.component";
|
||||||
|
import { OptionsInput } from "../shared/components/vault-filter-section.component";
|
||||||
|
import { OrganizationFilter } from "../shared/models/vault-filter.type";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-organization-options",
|
selector: "app-organization-options",
|
||||||
|
@ -24,9 +26,8 @@ export class OrganizationOptionsComponent {
|
||||||
policies: Policy[];
|
policies: Policy[];
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
@Input() organization: Organization;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(OptionsInput) private organization: OrganizationFilter,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="card vault-filters">
|
||||||
|
<div class="container loading-spinner" *ngIf="!isLoaded">
|
||||||
|
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isLoaded">
|
||||||
|
<div class="card-header d-flex">
|
||||||
|
{{ "filters" | i18n }}
|
||||||
|
<a
|
||||||
|
class="ml-auto"
|
||||||
|
href="https://bitwarden.com/help/searching-vault/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="{{ searchPlaceholder | i18n }}"
|
||||||
|
id="search"
|
||||||
|
class="form-control"
|
||||||
|
(input)="searchTextChanged($event.target.value)"
|
||||||
|
autocomplete="off"
|
||||||
|
appAutofocus
|
||||||
|
/>
|
||||||
|
<ng-container *ngFor="let f of filtersList">
|
||||||
|
<div class="filter">
|
||||||
|
<app-filter-section [activeFilter]="activeFilter" [section]="f"> </app-filter-section>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,351 @@
|
||||||
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { firstValueFrom, Subject, switchMap, takeUntil } 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 { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||||
|
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||||
|
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||||
|
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||||
|
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||||
|
|
||||||
|
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
|
||||||
|
import {
|
||||||
|
VaultFilterList,
|
||||||
|
VaultFilterSection,
|
||||||
|
VaultFilterType,
|
||||||
|
} from "../shared/models/vault-filter-section.type";
|
||||||
|
import { VaultFilter } from "../shared/models/vault-filter.model";
|
||||||
|
import {
|
||||||
|
CipherTypeFilter,
|
||||||
|
CollectionFilter,
|
||||||
|
FolderFilter,
|
||||||
|
OrganizationFilter,
|
||||||
|
} from "../shared/models/vault-filter.type";
|
||||||
|
|
||||||
|
import { OrganizationOptionsComponent } from "./organization-options.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-filter",
|
||||||
|
templateUrl: "vault-filter.component.html",
|
||||||
|
})
|
||||||
|
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<FolderView>();
|
||||||
|
|
||||||
|
isLoaded = false;
|
||||||
|
searchText = "";
|
||||||
|
|
||||||
|
protected destroy$: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
|
get filtersList() {
|
||||||
|
return this.filters ? Object.values(this.filters) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchPlaceholder() {
|
||||||
|
if (this.activeFilter.isFavorites) {
|
||||||
|
return "searchFavorites";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.isDeleted) {
|
||||||
|
return "searchTrash";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.cipherType === CipherType.Login) {
|
||||||
|
return "searchLogin";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.cipherType === CipherType.Card) {
|
||||||
|
return "searchCard";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.cipherType === CipherType.Identity) {
|
||||||
|
return "searchIdentity";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.cipherType === CipherType.SecureNote) {
|
||||||
|
return "searchSecureNote";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedFolderNode?.node) {
|
||||||
|
return "searchFolder";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedCollectionNode?.node) {
|
||||||
|
return "searchCollection";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.organizationId === "MyVault") {
|
||||||
|
return "searchMyVault";
|
||||||
|
}
|
||||||
|
if (this.activeFilter.organizationId) {
|
||||||
|
return "searchOrganization";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "searchVault";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected vaultFilterService: VaultFilterService,
|
||||||
|
protected policyService: PolicyService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService
|
||||||
|
) {
|
||||||
|
this.loadSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.filters = await this.buildAllFilters();
|
||||||
|
await this.applyTypeFilter(
|
||||||
|
(await firstValueFrom(this.filters?.typeFilter.data$)) as TreeNode<CipherTypeFilter>
|
||||||
|
);
|
||||||
|
this.isLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
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(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("disabledOrganizationFilterError")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filter = this.activeFilter;
|
||||||
|
filter.resetOrganization();
|
||||||
|
if (orgNode?.node.id !== "AllVaults") {
|
||||||
|
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> => {
|
||||||
|
this.onAddFolder.emit();
|
||||||
|
};
|
||||||
|
|
||||||
|
editFolder = async (folder: FolderFilter): Promise<void> => {
|
||||||
|
this.onEditFolder.emit(folder);
|
||||||
|
};
|
||||||
|
|
||||||
|
async getDefaultFilter(): Promise<TreeNode<VaultFilterType>> {
|
||||||
|
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();
|
||||||
|
builderFilter.typeFilter = await this.addTypeFilter();
|
||||||
|
builderFilter.folderFilter = await this.addFolderFilter();
|
||||||
|
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||||
|
builderFilter.trashFilter = await this.addTrashFilter();
|
||||||
|
return builderFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async addOrganizationFilter(): Promise<VaultFilterSection> {
|
||||||
|
const singleOrgPolicy = await this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
|
||||||
|
const personalVaultPolicy = await this.policyService.policyAppliesToUser(
|
||||||
|
PolicyType.PersonalOwnership
|
||||||
|
);
|
||||||
|
|
||||||
|
const addAction = !singleOrgPolicy
|
||||||
|
? { text: "newOrganization", route: "/create-organization" }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const orgFilterSection: VaultFilterSection = {
|
||||||
|
data$: this.vaultFilterService.organizationTree$,
|
||||||
|
header: {
|
||||||
|
showHeader: !(singleOrgPolicy && personalVaultPolicy),
|
||||||
|
isSelectable: true,
|
||||||
|
},
|
||||||
|
action: this.applyOrganizationFilter,
|
||||||
|
options: { component: OrganizationOptionsComponent },
|
||||||
|
add: addAction,
|
||||||
|
divider: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return orgFilterSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async addTypeFilter(): Promise<VaultFilterSection> {
|
||||||
|
const typeFilterSection: VaultFilterSection = {
|
||||||
|
data$: this.vaultFilterService.buildTypeTree(
|
||||||
|
{ id: "AllItems", name: "allItems", type: "all", icon: "" },
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
header: {
|
||||||
|
showHeader: true,
|
||||||
|
isSelectable: true,
|
||||||
|
},
|
||||||
|
action: this.applyTypeFilter,
|
||||||
|
};
|
||||||
|
return typeFilterSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async addFolderFilter(): Promise<VaultFilterSection> {
|
||||||
|
const folderFilterSection: VaultFilterSection = {
|
||||||
|
data$: this.vaultFilterService.folderTree$,
|
||||||
|
header: {
|
||||||
|
showHeader: true,
|
||||||
|
isSelectable: false,
|
||||||
|
},
|
||||||
|
action: this.applyFolderFilter,
|
||||||
|
edit: {
|
||||||
|
text: "editFolder",
|
||||||
|
action: this.editFolder,
|
||||||
|
},
|
||||||
|
add: {
|
||||||
|
text: "Add Folder",
|
||||||
|
action: this.addFolder,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return folderFilterSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async addCollectionFilter(): Promise<VaultFilterSection> {
|
||||||
|
const collectionFilterSection: VaultFilterSection = {
|
||||||
|
data$: this.vaultFilterService.collectionTree$,
|
||||||
|
header: {
|
||||||
|
showHeader: true,
|
||||||
|
isSelectable: true,
|
||||||
|
},
|
||||||
|
action: this.applyCollectionFilter,
|
||||||
|
};
|
||||||
|
return collectionFilterSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async addTrashFilter(): Promise<VaultFilterSection> {
|
||||||
|
const trashFilterSection: VaultFilterSection = {
|
||||||
|
data$: this.vaultFilterService.buildTypeTree(
|
||||||
|
{
|
||||||
|
id: "headTrash",
|
||||||
|
name: "HeadTrash",
|
||||||
|
type: "trash",
|
||||||
|
icon: "bwi-trash",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "trash",
|
||||||
|
name: this.i18nService.t("trash"),
|
||||||
|
type: "trash",
|
||||||
|
icon: "bwi-trash",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
header: {
|
||||||
|
showHeader: false,
|
||||||
|
isSelectable: true,
|
||||||
|
},
|
||||||
|
action: this.applyTypeFilter,
|
||||||
|
};
|
||||||
|
return trashFilterSection;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,186 +0,0 @@
|
||||||
<ng-container *ngIf="!hide">
|
|
||||||
<ng-container [ngSwitch]="displayMode">
|
|
||||||
<ng-container *ngSwitchCase="'noOrganizations'">
|
|
||||||
<ul class="filter-options">
|
|
||||||
<li class="filter-option active">
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button
|
|
||||||
class="filter-button"
|
|
||||||
[attr.aria-pressed]="activeFilter.myVaultOnly"
|
|
||||||
appA11yTitle="{{ 'vault' | i18n }}: {{ 'myVault' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
|
||||||
{{ "myVault" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="filter-option">
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<a href="#" routerLink="/create-organization" class="filter-button">
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "newOrganization" | i18n }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'personalOwnershipPolicy'">
|
|
||||||
<div class="filter-heading">
|
|
||||||
<button
|
|
||||||
(click)="toggleCollapse()"
|
|
||||||
title="{{ 'toggleCollapse' | i18n }}"
|
|
||||||
class="toggle-button"
|
|
||||||
[attr.aria-expanded]="!isCollapsed"
|
|
||||||
aria-controls="organization-filters"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-right': isCollapsed,
|
|
||||||
'bwi-angle-down': !isCollapsed
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="filter-button"
|
|
||||||
(click)="clearFilter()"
|
|
||||||
[ngClass]="{ active: !hasActiveFilter }"
|
|
||||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organizationGrouping.name | i18n }}"
|
|
||||||
>
|
|
||||||
{{ organizationGrouping.name | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
|
|
||||||
<li
|
|
||||||
class="filter-option"
|
|
||||||
*ngFor="let organization of organizations"
|
|
||||||
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button
|
|
||||||
class="filter-button"
|
|
||||||
(click)="applyOrganizationFilter(organization)"
|
|
||||||
[attr.aria-pressed]="activeFilter.selectedOrganizationId === organization.id"
|
|
||||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organization.name }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
|
||||||
{{ organization.name }}
|
|
||||||
</button>
|
|
||||||
<ng-container *ngIf="organization.id === activeFilter.selectedOrganizationId">
|
|
||||||
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">
|
|
||||||
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<bit-menu class="filter-organization-options" #orgMenu>
|
|
||||||
<app-organization-options [organization]="organization"></app-organization-options>
|
|
||||||
</bit-menu>
|
|
||||||
</ng-container>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="filter-option">
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<a href="#" routerLink="/create-organization" class="filter-button">
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "newOrganization" | i18n }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
|
|
||||||
<div class="filter-heading">
|
|
||||||
<button
|
|
||||||
class="filter-button active"
|
|
||||||
[attr.aria-pressed]="activeFilter.selectedOrganizationId === organizations[0].id"
|
|
||||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organizations[0].name }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
|
||||||
{{ organizations[0].name }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchDefault>
|
|
||||||
<div class="filter-heading">
|
|
||||||
<button
|
|
||||||
class="toggle-button"
|
|
||||||
title="{{ 'toggleCollapse' | i18n }}"
|
|
||||||
(click)="toggleCollapse()"
|
|
||||||
[attr.aria-expanded]="!isCollapsed"
|
|
||||||
aria-controls="organization-filters"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-right': isCollapsed,
|
|
||||||
'bwi-angle-down': !isCollapsed
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="filter-button"
|
|
||||||
(click)="clearFilter()"
|
|
||||||
[ngClass]="{ active: !hasActiveFilter }"
|
|
||||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organizationGrouping.name | i18n }}"
|
|
||||||
>
|
|
||||||
{{ organizationGrouping.name | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
|
|
||||||
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button
|
|
||||||
class="filter-button"
|
|
||||||
(click)="applyMyVaultFilter()"
|
|
||||||
appA11yTitle="{{ 'vault' | i18n }}: {{ 'myVault' | i18n }}"
|
|
||||||
[attr.aria-pressed]="activeFilter.myVaultOnly"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
|
||||||
{{ "myVault" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="filter-option"
|
|
||||||
*ngFor="let organization of organizations"
|
|
||||||
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button
|
|
||||||
class="filter-button"
|
|
||||||
[ngClass]="{ 'disabled-organization': !organization.enabled }"
|
|
||||||
(click)="applyOrganizationFilter(organization)"
|
|
||||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organization.name }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
|
||||||
{{ organization.name }}
|
|
||||||
</button>
|
|
||||||
<span class="ml-auto">
|
|
||||||
<i
|
|
||||||
*ngIf="!organization.enabled"
|
|
||||||
class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger"
|
|
||||||
aria-label="{{ 'organizationIsDisabled' | i18n }}"
|
|
||||||
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
|
|
||||||
></i
|
|
||||||
><button [bitMenuTriggerFor]="orgMenu" class="org-options">
|
|
||||||
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<bit-menu class="filter-organization-options" #orgMenu>
|
|
||||||
<app-organization-options [organization]="organization"></app-organization-options>
|
|
||||||
</bit-menu>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="filter-option" *ngIf="!(displayMode === 'singleOrganizationPolicy')">
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<a href="#" routerLink="/create-organization" class="filter-button">
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "newOrganization" | i18n }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
<hr />
|
|
||||||
</ng-container>
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { OrganizationFilterComponent as BaseOrganizationFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/organization-filter.component";
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
|
||||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-organization-filter",
|
|
||||||
templateUrl: "organization-filter.component.html",
|
|
||||||
})
|
|
||||||
export class OrganizationFilterComponent extends BaseOrganizationFilterComponent {
|
|
||||||
displayText = "allVaults";
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private platformUtilsService: PlatformUtilsService
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyOrganizationFilter(organization: Organization) {
|
|
||||||
if (organization.enabled) {
|
|
||||||
//proceed with default behaviour for enabled organizations
|
|
||||||
super.applyOrganizationFilter(organization);
|
|
||||||
} else {
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"error",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("disabledOrganizationFilterError")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/src/models/domain/organization";
|
||||||
|
import { TreeNode } from "@bitwarden/common/src/models/domain/treeNode";
|
||||||
|
import { CollectionView } from "@bitwarden/common/src/models/view/collectionView";
|
||||||
|
import { FolderView } from "@bitwarden/common/src/models/view/folderView";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FolderFilter,
|
||||||
|
CollectionFilter,
|
||||||
|
OrganizationFilter,
|
||||||
|
CipherTypeFilter,
|
||||||
|
} from "../../shared/models/vault-filter.type";
|
||||||
|
|
||||||
|
export abstract class VaultFilterService {
|
||||||
|
collapsedFilterNodes$: Observable<Set<string>>;
|
||||||
|
filteredFolders$: Observable<FolderView[]>;
|
||||||
|
filteredCollections$: Observable<CollectionView[]>;
|
||||||
|
organizationTree$: Observable<TreeNode<OrganizationFilter>>;
|
||||||
|
folderTree$: Observable<TreeNode<FolderFilter>>;
|
||||||
|
collectionTree$: Observable<TreeNode<CollectionFilter>>;
|
||||||
|
reloadCollections: () => Promise<void>;
|
||||||
|
setCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
|
||||||
|
expandOrgFilter: () => Promise<void>;
|
||||||
|
setOrganizationFilter: (organization: Organization) => void;
|
||||||
|
buildTypeTree: (
|
||||||
|
head: CipherTypeFilter,
|
||||||
|
array: CipherTypeFilter[]
|
||||||
|
) => Observable<TreeNode<CipherTypeFilter>>;
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, ReplaySubject, take } from "rxjs";
|
||||||
|
|
||||||
|
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||||
|
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||||
|
import { 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 { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||||
|
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||||
|
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||||
|
|
||||||
|
import { VaultFilterService } from "./vault-filter.service";
|
||||||
|
|
||||||
|
describe("vault filter service", () => {
|
||||||
|
let vaultFilterService: VaultFilterService;
|
||||||
|
|
||||||
|
let stateService: MockProxy<StateService>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
let folderService: MockProxy<FolderService>;
|
||||||
|
let cipherService: MockProxy<CipherService>;
|
||||||
|
let collectionService: MockProxy<CollectionService>;
|
||||||
|
let policyService: MockProxy<PolicyService>;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let organizations: ReplaySubject<Organization[]>;
|
||||||
|
let folderViews: ReplaySubject<FolderView[]>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stateService = mock<StateService>();
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
folderService = mock<FolderService>();
|
||||||
|
cipherService = mock<CipherService>();
|
||||||
|
collectionService = mock<CollectionService>();
|
||||||
|
policyService = mock<PolicyService>();
|
||||||
|
i18nService = mock<I18nService>();
|
||||||
|
|
||||||
|
organizations = new ReplaySubject<Organization[]>(1);
|
||||||
|
folderViews = new ReplaySubject<FolderView[]>(1);
|
||||||
|
|
||||||
|
organizationService.organizations$ = organizations;
|
||||||
|
folderService.folderViews$ = folderViews;
|
||||||
|
|
||||||
|
vaultFilterService = new VaultFilterService(
|
||||||
|
stateService,
|
||||||
|
organizationService,
|
||||||
|
folderService,
|
||||||
|
cipherService,
|
||||||
|
collectionService,
|
||||||
|
policyService,
|
||||||
|
i18nService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("collapsed filter nodes", () => {
|
||||||
|
const nodes = new Set(["1", "2"]);
|
||||||
|
it("updates observable when saving", (complete) => {
|
||||||
|
vaultFilterService.collapsedFilterNodes$.pipe(take(1)).subscribe((value) => {
|
||||||
|
if (value === nodes) {
|
||||||
|
complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
vaultFilterService.setCollapsedFilterNodes(nodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads from state on initialization", async () => {
|
||||||
|
stateService.getCollapsedGroupings.mockResolvedValue(["1", "2"]);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(vaultFilterService.collapsedFilterNodes$)).resolves.toEqual(
|
||||||
|
nodes
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("organizations", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const storedOrgs = [createOrganization("1", "org1"), createOrganization("2", "org2")];
|
||||||
|
organizations.next(storedOrgs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a nested tree", async () => {
|
||||||
|
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||||
|
|
||||||
|
expect(tree.children.length).toBe(3);
|
||||||
|
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||||
|
expect(tree.children.find((o) => o.node.name === "org2"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides My Vault if personal ownership policy is enabled", async () => {
|
||||||
|
policyService.policyAppliesToUser
|
||||||
|
.calledWith(PolicyType.PersonalOwnership)
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||||
|
|
||||||
|
expect(tree.children.length).toBe(2);
|
||||||
|
expect(!tree.children.find((o) => o.node.id === "MyVault"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 1 organization and My Vault if single organization policy is enabled", async () => {
|
||||||
|
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||||
|
|
||||||
|
expect(tree.children.length).toBe(2);
|
||||||
|
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||||
|
expect(tree.children.find((o) => o.node.id === "MyVault"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 1 organization if both single organization and personal ownership policies are enabled", async () => {
|
||||||
|
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
|
||||||
|
policyService.policyAppliesToUser
|
||||||
|
.calledWith(PolicyType.PersonalOwnership)
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||||
|
|
||||||
|
expect(tree.children.length).toBe(1);
|
||||||
|
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("folders", () => {
|
||||||
|
describe("filtered folders with organization", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Org must be updated before folderService else the subscription uses the null org default value
|
||||||
|
vaultFilterService.setOrganizationFilter(createOrganization("org test id", "Test Org"));
|
||||||
|
});
|
||||||
|
it("returns folders filtered by current organization", async () => {
|
||||||
|
const storedCiphers = [
|
||||||
|
createCipherView("1", "org test id", "folder test id"),
|
||||||
|
createCipherView("2", "non matching org id", "non matching folder id"),
|
||||||
|
];
|
||||||
|
cipherService.getAllDecrypted.mockResolvedValue(storedCiphers);
|
||||||
|
|
||||||
|
const storedFolders = [
|
||||||
|
createFolderView("folder test id", "test"),
|
||||||
|
createFolderView("non matching folder id", "test2"),
|
||||||
|
];
|
||||||
|
folderViews.next(storedFolders);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(vaultFilterService.filteredFolders$)).resolves.toEqual([
|
||||||
|
createFolderView("folder test id", "test"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("folder tree", () => {
|
||||||
|
it("returns a nested tree", async () => {
|
||||||
|
const storedFolders = [
|
||||||
|
createFolderView("Folder 1 Id", "Folder 1"),
|
||||||
|
createFolderView("Folder 2 Id", "Folder 1/Folder 2"),
|
||||||
|
createFolderView("Folder 3 Id", "Folder 1/Folder 3"),
|
||||||
|
];
|
||||||
|
folderViews.next(storedFolders);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(vaultFilterService.folderTree$);
|
||||||
|
|
||||||
|
expect(result.children[0].node.id === "Folder 1 Id");
|
||||||
|
expect(result.children[0].children.find((c) => c.node.id === "Folder 2 Id"));
|
||||||
|
expect(result.children[0].children.find((c) => c.node.id === "Folder 3 Id"));
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("collections", () => {
|
||||||
|
describe("filtered collections", () => {
|
||||||
|
it("returns collections filtered by current organization", async () => {
|
||||||
|
vaultFilterService.setOrganizationFilter(createOrganization("org test id", "Test Org"));
|
||||||
|
|
||||||
|
const storedCollections = [
|
||||||
|
createCollectionView("1", "collection 1", "org test id"),
|
||||||
|
createCollectionView("2", "collection 2", "non matching org id"),
|
||||||
|
];
|
||||||
|
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
|
||||||
|
vaultFilterService.reloadCollections();
|
||||||
|
|
||||||
|
await expect(firstValueFrom(vaultFilterService.filteredCollections$)).resolves.toEqual([
|
||||||
|
createCollectionView("1", "collection 1", "org test id"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("collection tree", () => {
|
||||||
|
it("returns a nested tree", async () => {
|
||||||
|
const storedCollections = [
|
||||||
|
createCollectionView("Collection 1 Id", "Collection 1", "org test id"),
|
||||||
|
createCollectionView("Collection 2 Id", "Collection 1/Collection 2", "org test id"),
|
||||||
|
createCollectionView("Collection 3 Id", "Collection 1/Collection 3", "org test id"),
|
||||||
|
];
|
||||||
|
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
|
||||||
|
vaultFilterService.reloadCollections();
|
||||||
|
|
||||||
|
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||||
|
|
||||||
|
expect(result.children[0].node.id === "Collection 1 Id");
|
||||||
|
expect(result.children[0].children.find((c) => c.node.id === "Collection 2 Id"));
|
||||||
|
expect(result.children[0].children.find((c) => c.node.id === "Collection 3 Id"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createOrganization(id: string, name: string) {
|
||||||
|
const org = new Organization();
|
||||||
|
org.id = id;
|
||||||
|
org.name = name;
|
||||||
|
org.identifier = name;
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCipherView(id: string, orgId: string, folderId: string) {
|
||||||
|
const cipher = new CipherView();
|
||||||
|
cipher.id = id;
|
||||||
|
cipher.organizationId = orgId;
|
||||||
|
cipher.folderId = folderId;
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFolderView(id: string, name: string): FolderView {
|
||||||
|
const folder = new FolderView();
|
||||||
|
folder.id = id;
|
||||||
|
folder.name = name;
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCollectionView(id: string, name: string, orgId: string): CollectionView {
|
||||||
|
const collection = new CollectionView();
|
||||||
|
collection.id = id;
|
||||||
|
collection.name = name;
|
||||||
|
collection.organizationId = orgId;
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,268 @@
|
||||||
|
import { Injectable, OnDestroy } from "@angular/core";
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatestWith,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
Subject,
|
||||||
|
takeUntil,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
ReplaySubject,
|
||||||
|
firstValueFrom,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||||
|
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||||
|
import { 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 { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||||
|
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||||
|
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||||
|
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FolderFilter,
|
||||||
|
CollectionFilter,
|
||||||
|
OrganizationFilter,
|
||||||
|
CipherTypeFilter,
|
||||||
|
} from "../shared/models/vault-filter.type";
|
||||||
|
|
||||||
|
import { VaultFilterService as VaultFilterServiceAbstraction } from "./abstractions/vault-filter.service";
|
||||||
|
|
||||||
|
const NestingDelimiter = "/";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VaultFilterService implements VaultFilterServiceAbstraction, OnDestroy {
|
||||||
|
protected _collapsedFilterNodes = new BehaviorSubject<Set<string>>(null);
|
||||||
|
collapsedFilterNodes$: Observable<Set<string>> = this._collapsedFilterNodes.pipe(
|
||||||
|
switchMap(async (nodes) => nodes ?? (await this.getCollapsedFilterNodes()))
|
||||||
|
);
|
||||||
|
|
||||||
|
organizationTree$: Observable<TreeNode<OrganizationFilter>> =
|
||||||
|
this.organizationService.organizations$.pipe(
|
||||||
|
switchMap((orgs) => this.buildOrganizationTree(orgs))
|
||||||
|
);
|
||||||
|
|
||||||
|
protected _filteredFolders = new ReplaySubject<FolderView[]>(1);
|
||||||
|
filteredFolders$: Observable<FolderView[]> = this._filteredFolders.asObservable();
|
||||||
|
protected _filteredCollections = new ReplaySubject<CollectionView[]>(1);
|
||||||
|
filteredCollections$: Observable<CollectionView[]> = this._filteredCollections.asObservable();
|
||||||
|
|
||||||
|
folderTree$: Observable<TreeNode<FolderFilter>> = this.filteredFolders$.pipe(
|
||||||
|
map((folders) => this.buildFolderTree(folders))
|
||||||
|
);
|
||||||
|
collectionTree$: Observable<TreeNode<CollectionFilter>> = this.filteredCollections$.pipe(
|
||||||
|
map((collections) => this.buildCollectionTree(collections))
|
||||||
|
);
|
||||||
|
|
||||||
|
protected _organizationFilter = new BehaviorSubject<Organization>(null);
|
||||||
|
protected destroy$: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
|
// TODO: Remove once collections is refactored with observables
|
||||||
|
protected collectionViews$ = new ReplaySubject<CollectionView[]>(1);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected stateService: StateService,
|
||||||
|
protected organizationService: OrganizationService,
|
||||||
|
protected folderService: FolderService,
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
protected collectionService: CollectionService,
|
||||||
|
protected policyService: PolicyService,
|
||||||
|
protected i18nService: I18nService
|
||||||
|
) {
|
||||||
|
this.loadSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadSubscriptions() {
|
||||||
|
this.folderService.folderViews$
|
||||||
|
.pipe(
|
||||||
|
combineLatestWith(this._organizationFilter),
|
||||||
|
switchMap(([folders, org]) => {
|
||||||
|
return this.filterFolders(folders, org);
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
.subscribe(this._filteredFolders);
|
||||||
|
|
||||||
|
// TODO: Use collectionService once collections is refactored
|
||||||
|
this.collectionViews$
|
||||||
|
.pipe(
|
||||||
|
combineLatestWith(this._organizationFilter),
|
||||||
|
switchMap(([collections, org]) => {
|
||||||
|
return this.filterCollections(collections, org);
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
.subscribe(this._filteredCollections);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove once collections is refactored with observables
|
||||||
|
async reloadCollections() {
|
||||||
|
this.collectionViews$.next(await this.collectionService.getAllDecrypted());
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
|
||||||
|
await this.stateService.setCollapsedGroupings(Array.from(collapsedFilterNodes));
|
||||||
|
this._collapsedFilterNodes.next(collapsedFilterNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getCollapsedFilterNodes(): Promise<Set<string>> {
|
||||||
|
const nodes = new Set(await this.stateService.getCollapsedGroupings());
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrganizationFilter(organization: Organization) {
|
||||||
|
if (organization?.id != "AllVaults") {
|
||||||
|
this._organizationFilter.next(organization);
|
||||||
|
} else {
|
||||||
|
this._organizationFilter.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async expandOrgFilter() {
|
||||||
|
const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$);
|
||||||
|
if (!collapsedFilterNodes.has("AllVaults")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
collapsedFilterNodes.delete("AllVaults");
|
||||||
|
await this.setCollapsedFilterNodes(collapsedFilterNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async buildOrganizationTree(
|
||||||
|
orgs?: Organization[]
|
||||||
|
): Promise<TreeNode<OrganizationFilter>> {
|
||||||
|
const headNode = this.getOrganizationFilterHead();
|
||||||
|
if (!(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership))) {
|
||||||
|
const myVaultNode = this.getOrganizationFilterMyVault();
|
||||||
|
headNode.children.push(myVaultNode);
|
||||||
|
}
|
||||||
|
if (await this.policyService.policyAppliesToUser(PolicyType.SingleOrg)) {
|
||||||
|
orgs = orgs.slice(0, 1);
|
||||||
|
}
|
||||||
|
if (orgs) {
|
||||||
|
orgs.forEach((org) => {
|
||||||
|
const orgCopy = org as OrganizationFilter;
|
||||||
|
orgCopy.icon = "bwi-business";
|
||||||
|
const node = new TreeNode<OrganizationFilter>(orgCopy, headNode.node, orgCopy.name);
|
||||||
|
headNode.children.push(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return headNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getOrganizationFilterHead(): TreeNode<OrganizationFilter> {
|
||||||
|
const head = new Organization() as OrganizationFilter;
|
||||||
|
head.enabled = true;
|
||||||
|
return new TreeNode<OrganizationFilter>(head, null, "allVaults", "AllVaults");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getOrganizationFilterMyVault(): TreeNode<OrganizationFilter> {
|
||||||
|
const myVault = new Organization() as OrganizationFilter;
|
||||||
|
myVault.id = "MyVault";
|
||||||
|
myVault.icon = "bwi-user";
|
||||||
|
myVault.enabled = true;
|
||||||
|
myVault.hideOptions = true;
|
||||||
|
return new TreeNode<OrganizationFilter>(myVault, null, this.i18nService.t("myVault"));
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypeTree(
|
||||||
|
head: CipherTypeFilter,
|
||||||
|
array?: CipherTypeFilter[]
|
||||||
|
): Observable<TreeNode<CipherTypeFilter>> {
|
||||||
|
const headNode = new TreeNode<CipherTypeFilter>(head, null);
|
||||||
|
array?.forEach((filter) => {
|
||||||
|
const node = new TreeNode<CipherTypeFilter>(filter, head, filter.name);
|
||||||
|
headNode.children.push(node);
|
||||||
|
});
|
||||||
|
return of(headNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async filterCollections(
|
||||||
|
storedCollections: CollectionView[],
|
||||||
|
org?: Organization
|
||||||
|
): Promise<CollectionView[]> {
|
||||||
|
return org?.id != null
|
||||||
|
? storedCollections.filter((c) => c.organizationId === org.id)
|
||||||
|
: storedCollections;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildCollectionTree(collections?: CollectionView[]): TreeNode<CollectionFilter> {
|
||||||
|
const headNode = this.getCollectionFilterHead();
|
||||||
|
if (!collections) {
|
||||||
|
return headNode;
|
||||||
|
}
|
||||||
|
const nodes: TreeNode<CollectionFilter>[] = [];
|
||||||
|
collections.forEach((c) => {
|
||||||
|
const collectionCopy = new CollectionView() as CollectionFilter;
|
||||||
|
collectionCopy.id = c.id;
|
||||||
|
collectionCopy.organizationId = c.organizationId;
|
||||||
|
collectionCopy.icon = "bwi-collection";
|
||||||
|
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||||
|
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
|
||||||
|
});
|
||||||
|
nodes.forEach((n) => {
|
||||||
|
n.parent = headNode.node;
|
||||||
|
headNode.children.push(n);
|
||||||
|
});
|
||||||
|
return headNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getCollectionFilterHead(): TreeNode<CollectionFilter> {
|
||||||
|
const head = new CollectionView() as CollectionFilter;
|
||||||
|
return new TreeNode<CollectionFilter>(head, null, "collections", "AllCollections");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async filterFolders(
|
||||||
|
storedFolders: FolderView[],
|
||||||
|
org?: Organization
|
||||||
|
): Promise<FolderView[]> {
|
||||||
|
if (org?.id == null) {
|
||||||
|
return storedFolders;
|
||||||
|
}
|
||||||
|
const ciphers = await this.cipherService.getAllDecrypted();
|
||||||
|
const orgCiphers = ciphers.filter((c) => c.organizationId == org?.id);
|
||||||
|
return storedFolders.filter(
|
||||||
|
(f) =>
|
||||||
|
orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 ||
|
||||||
|
ciphers.filter((c) => c.folderId == f.id).length < 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildFolderTree(folders?: FolderView[]): TreeNode<FolderFilter> {
|
||||||
|
const headNode = this.getFolderFilterHead();
|
||||||
|
if (!folders) {
|
||||||
|
return headNode;
|
||||||
|
}
|
||||||
|
const nodes: TreeNode<FolderFilter>[] = [];
|
||||||
|
folders.forEach((f) => {
|
||||||
|
const folderCopy = new FolderView() as FolderFilter;
|
||||||
|
folderCopy.id = f.id;
|
||||||
|
folderCopy.revisionDate = f.revisionDate;
|
||||||
|
folderCopy.icon = "bwi-folder";
|
||||||
|
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||||
|
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach((n) => {
|
||||||
|
n.parent = headNode.node;
|
||||||
|
headNode.children.push(n);
|
||||||
|
});
|
||||||
|
return headNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getFolderFilterHead(): TreeNode<FolderFilter> {
|
||||||
|
const head = new FolderView() as FolderFilter;
|
||||||
|
return new TreeNode<FolderFilter>(head, null, "folders", "AllFolders");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
<ng-container *ngIf="show">
|
|
||||||
<div class="filter-heading">
|
|
||||||
<button
|
|
||||||
(click)="toggleCollapse(collectionsGrouping)"
|
|
||||||
[attr.aria-expanded]="!isCollapsed(collectionsGrouping)"
|
|
||||||
aria-controls="collection-filters"
|
|
||||||
title="{{ 'toggleCollapse' | i18n }}"
|
|
||||||
class="toggle-button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-right': isCollapsed(collectionsGrouping),
|
|
||||||
'bwi-angle-down': !isCollapsed(collectionsGrouping)
|
|
||||||
}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<h3 class="filter-title"> {{ collectionsGrouping.name | i18n }}</h3>
|
|
||||||
</div>
|
|
||||||
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
|
|
||||||
<ng-template #recursiveCollections let-collections>
|
|
||||||
<li
|
|
||||||
*ngFor="let c of collections"
|
|
||||||
[ngClass]="{
|
|
||||||
active: c.node.id === activeFilter.selectedCollectionId && activeFilter.selectedCollection
|
|
||||||
}"
|
|
||||||
class="filter-option"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button
|
|
||||||
class="toggle-button"
|
|
||||||
*ngIf="c.children.length"
|
|
||||||
(click)="toggleCollapse(c.node)"
|
|
||||||
title="{{ 'toggleCollapse' | i18n }}"
|
|
||||||
[attr.aria-expanded]="!isCollapsed(c.node)"
|
|
||||||
[attr.aria-controls]="c.node.name + '_children'"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-right': isCollapsed(c.node),
|
|
||||||
'bwi-angle-down': !isCollapsed(c.node)
|
|
||||||
}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<button class="filter-button" (click)="applyFilter(c.node)">
|
|
||||||
<i
|
|
||||||
*ngIf="c.children.length === 0"
|
|
||||||
class="bwi bwi-collection bwi-fw"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i
|
|
||||||
> {{ c.node.name }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<ul
|
|
||||||
[id]="c.node.name + '_children'"
|
|
||||||
class="nested-filter-options"
|
|
||||||
*ngIf="c.children.length && !isCollapsed(c.node)"
|
|
||||||
>
|
|
||||||
<ng-container
|
|
||||||
*ngTemplateOutlet="recursiveCollections; context: { $implicit: c.children }"
|
|
||||||
>
|
|
||||||
</ng-container>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ng-template>
|
|
||||||
<ng-container
|
|
||||||
*ngTemplateOutlet="recursiveCollections; context: { $implicit: nestedCollections }"
|
|
||||||
>
|
|
||||||
</ng-container>
|
|
||||||
</ul>
|
|
||||||
</ng-container>
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { CollectionFilterComponent as BaseCollectionFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/collection-filter.component";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-collection-filter",
|
|
||||||
templateUrl: "collection-filter.component.html",
|
|
||||||
})
|
|
||||||
export class CollectionFilterComponent extends BaseCollectionFilterComponent {}
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
<ng-container *ngIf="filters && filters.length">
|
||||||
|
<div *ngIf="headerInfo.showHeader" class="filter-heading">
|
||||||
|
<button
|
||||||
|
class="toggle-button"
|
||||||
|
(click)="toggleCollapse(headerNode.node)"
|
||||||
|
[attr.aria-expanded]="!isCollapsed(headerNode.node)"
|
||||||
|
aria-controls="sub-filters"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="isCollapsed(headerNode.node) ? 'bwi-angle-right' : 'bwi-angle-down'"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="headerInfo.isSelectable"
|
||||||
|
class="filter-button"
|
||||||
|
(click)="onFilterSelect(headerNode)"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
[ngClass]="{
|
||||||
|
active: isAllVaultsSelected || isNodeSelected(headerNode)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ headerNode.node.name | i18n }}
|
||||||
|
</h3>
|
||||||
|
</button>
|
||||||
|
<h3 *ngIf="!headerInfo.isSelectable" class="filter-title">
|
||||||
|
{{ headerNode.node.name | i18n }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="showAddButton"
|
||||||
|
(click)="onAdd()"
|
||||||
|
class="text-muted ml-auto add-button"
|
||||||
|
appA11yTitle="{{ addInfo.text | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
id="{{ headerNode.node.name }}-filters"
|
||||||
|
*ngIf="!isCollapsed(headerNode.node)"
|
||||||
|
class="filter-options"
|
||||||
|
>
|
||||||
|
<ng-template #recursiveFilters let-filters>
|
||||||
|
<li
|
||||||
|
*ngFor="let f of filters"
|
||||||
|
[ngClass]="{
|
||||||
|
active: isNodeSelected(f)
|
||||||
|
}"
|
||||||
|
class="filter-option"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button
|
||||||
|
*ngIf="f.children.length"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
appA11yTitle="{{ 'toggleCollapse' | i18n }} {{ f.node.name | i18n }}"
|
||||||
|
(click)="toggleCollapse(f.node)"
|
||||||
|
[attr.aria-expanded]="!isCollapsed(f.node)"
|
||||||
|
[attr.aria-controls]="f.node.name + '_children'"
|
||||||
|
class="toggle-button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed(f.node),
|
||||||
|
'bwi-angle-down': !isCollapsed(f.node)
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-button"
|
||||||
|
appA11yTitle="{{ 'vault' | i18n }}: {{ f.node.name | i18n }}"
|
||||||
|
[ngClass]="{ 'disabled-organization': isOrganization && !f.node.enabled }"
|
||||||
|
(click)="onFilterSelect(f)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
*ngIf="f.children.length === 0"
|
||||||
|
class="bwi bwi-fw {{ f.node.icon }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
{{ f.node.name }}
|
||||||
|
</button>
|
||||||
|
<span class="ml-auto">
|
||||||
|
<button
|
||||||
|
*ngIf="editInfo && f.node.id"
|
||||||
|
class="edit-button"
|
||||||
|
(click)="onEdit(f)"
|
||||||
|
appA11yTitle="{{ editInfo.text | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<i
|
||||||
|
*ngIf="isOrganizationFilter && !f.node.enabled"
|
||||||
|
class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger"
|
||||||
|
aria-label="{{ 'organizationIsDisabled' | i18n }}"
|
||||||
|
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
|
||||||
|
></i
|
||||||
|
><ng-container *ngIf="optionsInfo && !f.node.hideOptions"
|
||||||
|
><button [bitMenuTriggerFor]="optionsMenu" class="filter-options-icon">
|
||||||
|
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<bit-menu class="filter-organization-options" #optionsMenu>
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
|
||||||
|
></ng-container>
|
||||||
|
</bit-menu>
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ul
|
||||||
|
[id]="f.node.name + '_children'"
|
||||||
|
class="nested-filter-options"
|
||||||
|
*ngIf="f.children.length && !isCollapsed(f.node)"
|
||||||
|
>
|
||||||
|
<ng-container *ngTemplateOutlet="recursiveFilters; context: { $implicit: f.children }">
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ng-template>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="recursiveFilters; context: { $implicit: filters }"
|
||||||
|
></ng-container>
|
||||||
|
<li class="filter-option" *ngIf="showAddLink">
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<a href="#" routerLink="{{ addInfo.route }}" class="filter-button">
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
{{ addInfo.text | i18n }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr *ngIf="divider" />
|
||||||
|
</ng-container>
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||||
|
|
||||||
|
import { VaultFilterService } from "../../services/abstractions/vault-filter.service";
|
||||||
|
import { VaultFilterSection, VaultFilterType } from "../models/vault-filter-section.type";
|
||||||
|
import { VaultFilter } from "../models/vault-filter.model";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-filter-section",
|
||||||
|
templateUrl: "vault-filter-section.component.html",
|
||||||
|
})
|
||||||
|
export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@Input() activeFilter: VaultFilter;
|
||||||
|
@Input() section: VaultFilterSection;
|
||||||
|
|
||||||
|
data: TreeNode<VaultFilterType>;
|
||||||
|
collapsedFilterNodes: Set<string> = new Set();
|
||||||
|
|
||||||
|
private injectors = new Map<string, Injector>();
|
||||||
|
|
||||||
|
constructor(private vaultFilterService: VaultFilterService, private injector: Injector) {
|
||||||
|
this.vaultFilterService.collapsedFilterNodes$
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((nodes) => {
|
||||||
|
this.collapsedFilterNodes = nodes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.section?.data$?.pipe(takeUntil(this.destroy$)).subscribe((data) => {
|
||||||
|
this.data = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerNode() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerInfo() {
|
||||||
|
return this.section.header;
|
||||||
|
}
|
||||||
|
|
||||||
|
get filters() {
|
||||||
|
return this.data?.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOrganizationFilter() {
|
||||||
|
return this.data.node instanceof Organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAllVaultsSelected() {
|
||||||
|
return this.isOrganizationFilter && !this.activeFilter.selectedOrganizationNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNodeSelected(filterNode: TreeNode<VaultFilterType>) {
|
||||||
|
return (
|
||||||
|
this.activeFilter.organizationId === filterNode?.node.id ||
|
||||||
|
this.activeFilter.cipherTypeId === filterNode?.node.id ||
|
||||||
|
this.activeFilter.folderId === filterNode?.node.id ||
|
||||||
|
this.activeFilter.collectionId === filterNode?.node.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onFilterSelect(filterNode: TreeNode<VaultFilterType>) {
|
||||||
|
await this.section?.action(filterNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
get editInfo() {
|
||||||
|
return this.section?.edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEdit(filterNode: TreeNode<VaultFilterType>) {
|
||||||
|
this.section?.edit?.action(filterNode.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
get addInfo() {
|
||||||
|
return this.section.add;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showAddButton() {
|
||||||
|
return this.section.add && !this.section.add.route;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showAddLink() {
|
||||||
|
return this.section.add && this.section.add.route;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onAdd() {
|
||||||
|
this.section?.add?.action();
|
||||||
|
}
|
||||||
|
|
||||||
|
get optionsInfo() {
|
||||||
|
return this.section?.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
get divider() {
|
||||||
|
return this.section?.divider;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCollapsed(node: ITreeNodeObject) {
|
||||||
|
return this.collapsedFilterNodes.has(node.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleCollapse(node: ITreeNodeObject) {
|
||||||
|
if (this.collapsedFilterNodes.has(node.id)) {
|
||||||
|
this.collapsedFilterNodes.delete(node.id);
|
||||||
|
} else {
|
||||||
|
this.collapsedFilterNodes.add(node.id);
|
||||||
|
}
|
||||||
|
await this.vaultFilterService.setCollapsedFilterNodes(this.collapsedFilterNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// an injector is necessary to pass data into a dynamic component
|
||||||
|
// here we are creating a new injector for each filter that has options
|
||||||
|
createInjector(data: VaultFilterType) {
|
||||||
|
let inject = this.injectors.get(data.id);
|
||||||
|
if (!inject) {
|
||||||
|
inject = Injector.create({
|
||||||
|
providers: [{ provide: OptionsInput, useValue: data }],
|
||||||
|
parent: this.injector,
|
||||||
|
});
|
||||||
|
this.injectors.set(data.id, inject);
|
||||||
|
}
|
||||||
|
return inject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const OptionsInput = new InjectionToken<VaultFilterType>("OptionsInput");
|
|
@ -1,82 +0,0 @@
|
||||||
<ng-container *ngIf="!hide">
|
|
||||||
<div class="filter-heading">
|
|
||||||
<button
|
|
||||||
class="toggle-button"
|
|
||||||
(click)="toggleCollapse(foldersGrouping)"
|
|
||||||
[attr.aria-expanded]="!isCollapsed(foldersGrouping)"
|
|
||||||
aria-controls="folder-filters"
|
|
||||||
title="{{ 'toggleCollapse' | i18n }}"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-right': isCollapsed(foldersGrouping),
|
|
||||||
'bwi-angle-down': !isCollapsed(foldersGrouping)
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<h3 class="filter-title"> {{ "folders" | i18n }}</h3>
|
|
||||||
<button
|
|
||||||
class="text-muted ml-auto add-button"
|
|
||||||
(click)="addFolder()"
|
|
||||||
appA11yTitle="{{ 'addFolder' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul id="folder-filters" *ngIf="!isCollapsed(foldersGrouping)" class="filter-options">
|
|
||||||
<ng-template #recursiveFolders let-folders>
|
|
||||||
<li
|
|
||||||
*ngFor="let f of folders"
|
|
||||||
[ngClass]="{
|
|
||||||
active: f.node.id === activeFilter.selectedFolderId && activeFilter.selectedFolder
|
|
||||||
}"
|
|
||||||
class="filter-option"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button
|
|
||||||
*ngIf="f.children.length"
|
|
||||||
title="{{ 'toggleCollapse' | i18n }}"
|
|
||||||
(click)="toggleCollapse(f.node)"
|
|
||||||
[attr.aria-expanded]="!isCollapsed(f.node)"
|
|
||||||
[attr.aria-controls]="f.node.name + '_children'"
|
|
||||||
class="toggle-button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-right': isCollapsed(f.node),
|
|
||||||
'bwi-angle-down': !isCollapsed(f.node)
|
|
||||||
}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<button class="filter-button" (click)="applyFilter(f.node)">
|
|
||||||
<i *ngIf="f.children.length === 0" class="bwi bwi-fw bwi-folder" aria-hidden="true"></i
|
|
||||||
> {{ f.node.name }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="edit-button"
|
|
||||||
(click)="editFolder(f.node)"
|
|
||||||
appA11yTitle="{{ 'editFolder' | i18n }}"
|
|
||||||
*ngIf="f.node.id"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<ul
|
|
||||||
[id]="f.node.name + '_children'"
|
|
||||||
class="nested-filter-options"
|
|
||||||
*ngIf="f.children.length && !isCollapsed(f.node)"
|
|
||||||
>
|
|
||||||
<ng-container *ngTemplateOutlet="recursiveFolders; context: { $implicit: f.children }">
|
|
||||||
</ng-container>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ng-template>
|
|
||||||
<ng-container
|
|
||||||
*ngTemplateOutlet="recursiveFolders; context: { $implicit: nestedFolders }"
|
|
||||||
></ng-container>
|
|
||||||
</ul>
|
|
||||||
</ng-container>
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { FolderFilterComponent as BaseFolderFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/folder-filter.component";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-folder-filter",
|
|
||||||
templateUrl: "folder-filter.component.html",
|
|
||||||
})
|
|
||||||
export class FolderFilterComponent extends BaseFolderFilterComponent {}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { TreeNode } from "@bitwarden/common/src/models/domain/treeNode";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CipherTypeFilter,
|
||||||
|
CollectionFilter,
|
||||||
|
FolderFilter,
|
||||||
|
OrganizationFilter,
|
||||||
|
} from "./vault-filter.type";
|
||||||
|
|
||||||
|
export type VaultFilterType =
|
||||||
|
| OrganizationFilter
|
||||||
|
| CipherTypeFilter
|
||||||
|
| FolderFilter
|
||||||
|
| CollectionFilter;
|
||||||
|
|
||||||
|
export enum VaultFilterLabel {
|
||||||
|
OrganizationFilter = "organizationFilter",
|
||||||
|
TypeFilter = "typeFilter",
|
||||||
|
FolderFilter = "folderFilter",
|
||||||
|
CollectionFilter = "collectionFilter",
|
||||||
|
TrashFilter = "trashFilter",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VaultFilterSection = {
|
||||||
|
data$: Observable<TreeNode<VaultFilterType>>;
|
||||||
|
header: {
|
||||||
|
showHeader: boolean;
|
||||||
|
isSelectable: boolean;
|
||||||
|
};
|
||||||
|
action: (filterNode: TreeNode<VaultFilterType>) => Promise<void>;
|
||||||
|
edit?: {
|
||||||
|
text: string;
|
||||||
|
action: (filter: VaultFilterType) => void;
|
||||||
|
};
|
||||||
|
add?: {
|
||||||
|
text: string;
|
||||||
|
route?: string;
|
||||||
|
action?: () => void;
|
||||||
|
};
|
||||||
|
options?: {
|
||||||
|
component: any;
|
||||||
|
};
|
||||||
|
divider?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VaultFilterList = {
|
||||||
|
[key in VaultFilterLabel]?: VaultFilterSection;
|
||||||
|
};
|
|
@ -0,0 +1,332 @@
|
||||||
|
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||||
|
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||||
|
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||||
|
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||||
|
|
||||||
|
import { VaultFilter } from "./vault-filter.model";
|
||||||
|
import {
|
||||||
|
CipherTypeFilter,
|
||||||
|
CollectionFilter,
|
||||||
|
FolderFilter,
|
||||||
|
OrganizationFilter,
|
||||||
|
} from "./vault-filter.type";
|
||||||
|
|
||||||
|
describe("VaultFilter", () => {
|
||||||
|
describe("filterFunction", () => {
|
||||||
|
const allCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||||
|
{
|
||||||
|
id: "AllItems",
|
||||||
|
name: "allItems",
|
||||||
|
type: "all",
|
||||||
|
icon: "",
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const favoriteCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||||
|
{
|
||||||
|
id: "favorites",
|
||||||
|
name: "favorites",
|
||||||
|
type: "favorites",
|
||||||
|
icon: "bwi-star",
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const identityCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||||
|
{
|
||||||
|
id: "identity",
|
||||||
|
name: "identity",
|
||||||
|
type: CipherType.Identity,
|
||||||
|
icon: "bwi-id-card",
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const trashFilter = new TreeNode<CipherTypeFilter>(
|
||||||
|
{
|
||||||
|
id: "trash",
|
||||||
|
name: "trash",
|
||||||
|
type: "trash",
|
||||||
|
icon: "bwi-trash",
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
describe("generic cipher", () => {
|
||||||
|
it("should return true when no filter is applied", () => {
|
||||||
|
const cipher = createCipher();
|
||||||
|
const filterFunction = createFilterFunction({});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a favorite cipher", () => {
|
||||||
|
const cipher = createCipher({ favorite: true });
|
||||||
|
|
||||||
|
it("should return true when filtering for favorites", () => {
|
||||||
|
const filterFunction = createFilterFunction({ selectedCipherTypeNode: allCiphersFilter });
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filtering for trash", () => {
|
||||||
|
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a deleted cipher", () => {
|
||||||
|
const cipher = createCipher({ deletedDate: new Date() });
|
||||||
|
|
||||||
|
it("should return true when filtering for trash", () => {
|
||||||
|
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filtering for favorites", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCipherTypeNode: favoriteCiphersFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a cipher with type", () => {
|
||||||
|
it("should return true when filter matches cipher type", () => {
|
||||||
|
const cipher = createCipher({ type: CipherType.Identity });
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCipherTypeNode: identityCiphersFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filter does not match cipher type", () => {
|
||||||
|
const cipher = createCipher({ type: CipherType.Card });
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCipherTypeNode: identityCiphersFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a cipher with folder id", () => {
|
||||||
|
it("should return true when filter matches folder id", () => {
|
||||||
|
const cipher = createCipher({ folderId: "folderId" });
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedFolderNode: createFolderFilterNode({ id: "folderId" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filter does not match folder id", () => {
|
||||||
|
const cipher = createCipher({ folderId: "folderId" });
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedFolderNode: createFolderFilterNode({ id: "differentFolderId" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a cipher without folder", () => {
|
||||||
|
const cipher = createCipher({ folderId: null });
|
||||||
|
|
||||||
|
it("should return true when filtering on unassigned folder", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedFolderNode: createFolderFilterNode({ id: null }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given an organizational cipher (with organization and collections)", () => {
|
||||||
|
const cipher = createCipher({
|
||||||
|
organizationId: "organizationId",
|
||||||
|
collectionIds: ["collectionId", "anotherId"],
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true when filter matches collection id", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCollectionNode: createCollectionFilterNode({
|
||||||
|
id: "collectionId",
|
||||||
|
organizationId: "organizationId",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filter does not match collection id", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCollectionNode: createCollectionFilterNode({
|
||||||
|
id: "nonMatchingCollectionId",
|
||||||
|
organizationId: "organizationId",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filter does not match organization id", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedOrganizationNode: createOrganizationFilterNode({
|
||||||
|
id: "nonMatchingOrganizationId",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filtering for my vault only", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedOrganizationNode: createOrganizationFilterNode({ id: "MyVault" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when filtering by All Collections", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCollectionNode: createCollectionFilterNode({ id: "AllCollections" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given an unassigned organizational cipher (with organization, without collection)", () => {
|
||||||
|
const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] });
|
||||||
|
|
||||||
|
it("should return true when filtering for unassigned collection", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCollectionNode: createCollectionFilterNode({ id: null }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true when filter matches organization id", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedOrganizationNode: createOrganizationFilterNode({ id: "organizationId" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given an individual cipher (without organization or collection)", () => {
|
||||||
|
const cipher = createCipher({ organizationId: null, collectionIds: [] });
|
||||||
|
|
||||||
|
it("should return false when filtering for unassigned collection", () => {
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedCollectionNode: createCollectionFilterNode({ id: null }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true when filtering for my vault only", () => {
|
||||||
|
const cipher = createCipher({ organizationId: null });
|
||||||
|
const filterFunction = createFilterFunction({
|
||||||
|
selectedOrganizationNode: createOrganizationFilterNode({ id: "MyVault" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = filterFunction(cipher);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createFilterFunction(options: Partial<VaultFilter> = {}) {
|
||||||
|
return new VaultFilter(options).buildFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOrganizationFilterNode(
|
||||||
|
options: Partial<OrganizationFilter>
|
||||||
|
): TreeNode<OrganizationFilter> {
|
||||||
|
const org = new Organization() as OrganizationFilter;
|
||||||
|
org.id = options.id;
|
||||||
|
org.icon = options.icon ?? "";
|
||||||
|
return new TreeNode<OrganizationFilter>(org, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFolderFilterNode(options: Partial<FolderFilter>): TreeNode<FolderFilter> {
|
||||||
|
const folder = new FolderView() as FolderFilter;
|
||||||
|
folder.id = options.id;
|
||||||
|
folder.name = options.name;
|
||||||
|
folder.icon = options.icon ?? "";
|
||||||
|
folder.revisionDate = options.revisionDate ?? new Date();
|
||||||
|
return new TreeNode<FolderFilter>(folder, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCollectionFilterNode(
|
||||||
|
options: Partial<CollectionFilter>
|
||||||
|
): TreeNode<CollectionFilter> {
|
||||||
|
const collection = new CollectionView() as CollectionFilter;
|
||||||
|
collection.id = options.id;
|
||||||
|
collection.name = options.name ?? "";
|
||||||
|
collection.icon = options.icon ?? "";
|
||||||
|
collection.organizationId = options.organizationId;
|
||||||
|
collection.externalId = options.externalId ?? "";
|
||||||
|
collection.readOnly = options.readOnly ?? false;
|
||||||
|
collection.hidePasswords = options.hidePasswords ?? false;
|
||||||
|
return new TreeNode<CollectionFilter>(collection, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCipher(options: Partial<CipherView> = {}) {
|
||||||
|
const cipher = new CipherView();
|
||||||
|
|
||||||
|
cipher.favorite = options.favorite ?? false;
|
||||||
|
cipher.deletedDate = options.deletedDate;
|
||||||
|
cipher.type = options.type;
|
||||||
|
cipher.folderId = options.folderId;
|
||||||
|
cipher.collectionIds = options.collectionIds;
|
||||||
|
cipher.organizationId = options.organizationId;
|
||||||
|
|
||||||
|
return cipher;
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||||
|
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||||
|
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CipherStatus,
|
||||||
|
CipherTypeFilter,
|
||||||
|
CollectionFilter,
|
||||||
|
FolderFilter,
|
||||||
|
OrganizationFilter,
|
||||||
|
} from "./vault-filter.type";
|
||||||
|
|
||||||
|
export type VaultFilterFunction = (cipher: CipherView) => boolean;
|
||||||
|
|
||||||
|
// TODO: Replace shared VaultFilter Model with this one and
|
||||||
|
// refactor browser and desktop code to use this model.
|
||||||
|
export class VaultFilter {
|
||||||
|
selectedOrganizationNode: TreeNode<OrganizationFilter>;
|
||||||
|
selectedCipherTypeNode: TreeNode<CipherTypeFilter>;
|
||||||
|
selectedFolderNode: TreeNode<FolderFilter>;
|
||||||
|
selectedCollectionNode: TreeNode<CollectionFilter>;
|
||||||
|
|
||||||
|
get isFavorites(): boolean {
|
||||||
|
return this.selectedCipherTypeNode?.node.type === "favorites";
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDeleted(): boolean {
|
||||||
|
return this.selectedCipherTypeNode?.node.type === "trash" ? true : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get organizationId(): string {
|
||||||
|
return this.selectedOrganizationNode?.node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cipherType(): CipherType {
|
||||||
|
return this.selectedCipherTypeNode?.node.type in CipherType
|
||||||
|
? (this.selectedCipherTypeNode?.node.type as CipherType)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cipherStatus(): CipherStatus {
|
||||||
|
return this.selectedCipherTypeNode?.node.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cipherTypeId(): string {
|
||||||
|
return this.selectedCipherTypeNode?.node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get folderId(): string {
|
||||||
|
return this.selectedFolderNode?.node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get collectionId(): string {
|
||||||
|
return this.selectedCollectionNode?.node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(init?: Partial<VaultFilter>) {
|
||||||
|
Object.assign(this, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFilter() {
|
||||||
|
this.selectedCipherTypeNode = null;
|
||||||
|
this.selectedFolderNode = null;
|
||||||
|
this.selectedCollectionNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetOrganization() {
|
||||||
|
this.selectedOrganizationNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFilter(): VaultFilterFunction {
|
||||||
|
return (cipher) => {
|
||||||
|
let cipherPassesFilter = true;
|
||||||
|
if (this.isFavorites && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.favorite;
|
||||||
|
}
|
||||||
|
if (this.isDeleted && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.isDeleted;
|
||||||
|
}
|
||||||
|
if (this.cipherType && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.type === this.cipherType;
|
||||||
|
}
|
||||||
|
if (this.selectedFolderNode) {
|
||||||
|
// No folder
|
||||||
|
if (this.folderId === null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.folderId === null;
|
||||||
|
}
|
||||||
|
// Folder
|
||||||
|
if (this.folderId !== null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.folderId === this.folderId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.selectedCollectionNode) {
|
||||||
|
// All Collections
|
||||||
|
if (this.collectionId === "AllCollections" && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = false;
|
||||||
|
}
|
||||||
|
// Unassigned
|
||||||
|
if (this.collectionId === null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter =
|
||||||
|
cipher.organizationId != null &&
|
||||||
|
(cipher.collectionIds == null || cipher.collectionIds.length === 0);
|
||||||
|
}
|
||||||
|
// Collection
|
||||||
|
if (
|
||||||
|
this.collectionId !== null &&
|
||||||
|
this.collectionId !== "AllCollections" &&
|
||||||
|
cipherPassesFilter
|
||||||
|
) {
|
||||||
|
cipherPassesFilter =
|
||||||
|
cipher.collectionIds != null && cipher.collectionIds.includes(this.collectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.selectedOrganizationNode) {
|
||||||
|
// My Vault
|
||||||
|
if (this.organizationId === "MyVault" && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.organizationId === null;
|
||||||
|
}
|
||||||
|
// Organization
|
||||||
|
else if (this.organizationId !== null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.organizationId === this.organizationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cipherPassesFilter;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { CipherType } from "@bitwarden/common/src/enums/cipherType";
|
||||||
|
import { Organization } from "@bitwarden/common/src/models/domain/organization";
|
||||||
|
import { ITreeNodeObject } from "@bitwarden/common/src/models/domain/treeNode";
|
||||||
|
import { CollectionView } from "@bitwarden/common/src/models/view/collectionView";
|
||||||
|
import { FolderView } from "@bitwarden/common/src/models/view/folderView";
|
||||||
|
|
||||||
|
export type CipherStatus = "all" | "favorites" | "trash" | CipherType;
|
||||||
|
|
||||||
|
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
|
||||||
|
export type CollectionFilter = CollectionView & { icon: string };
|
||||||
|
export type FolderFilter = FolderView & { icon: string };
|
||||||
|
export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean };
|
|
@ -1,33 +0,0 @@
|
||||||
<ng-container *ngIf="show">
|
|
||||||
<ul class="filter-options">
|
|
||||||
<li class="filter-option" [ngClass]="{ active: activeFilter.status === 'all' }">
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button class="filter-button" (click)="applyFilter('all')">
|
|
||||||
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i> {{ "allItems" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
*ngIf="!hideFavorites"
|
|
||||||
class="filter-option"
|
|
||||||
[ngClass]="{ active: activeFilter.status === 'favorites' }"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button class="filter-button" (click)="applyFilter('favorites')">
|
|
||||||
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i> {{ "favorites" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
*ngIf="!hideTrash"
|
|
||||||
class="filter-option"
|
|
||||||
[ngClass]="{ active: activeFilter.status === 'trash' }"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button class="filter-button" (click)="applyFilter('trash')">
|
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i> {{ "trash" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ng-container>
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { StatusFilterComponent as BaseStatusFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/status-filter.component";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-status-filter",
|
|
||||||
templateUrl: "status-filter.component.html",
|
|
||||||
})
|
|
||||||
export class StatusFilterComponent extends BaseStatusFilterComponent {}
|
|
|
@ -1,60 +0,0 @@
|
||||||
<div class="filter-heading">
|
|
||||||
<button
|
|
||||||
class="toggle-button"
|
|
||||||
[attr.aria-expanded]="!isCollapsed"
|
|
||||||
aria-controls="type-filters"
|
|
||||||
(click)="toggleCollapse()"
|
|
||||||
title="{{ 'toggleCollapse' | i18n }}"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-right': isCollapsed,
|
|
||||||
'bwi-angle-down': !isCollapsed
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<h3> {{ "types" | i18n }}</h3>
|
|
||||||
</div>
|
|
||||||
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
|
|
||||||
<li
|
|
||||||
class="filter-option"
|
|
||||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Login }"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Login)">
|
|
||||||
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i> {{ "typeLogin" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Card)">
|
|
||||||
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i> {{ "typeCard" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="filter-option"
|
|
||||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Identity }"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Identity)">
|
|
||||||
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i> {{ "typeIdentity" | i18n }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="filter-option"
|
|
||||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SecureNote }"
|
|
||||||
>
|
|
||||||
<span class="filter-buttons">
|
|
||||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)">
|
|
||||||
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i> {{
|
|
||||||
"typeSecureNote" | i18n
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-type-filter",
|
|
||||||
templateUrl: "type-filter.component.html",
|
|
||||||
})
|
|
||||||
export class TypeFilterComponent extends BaseTypeFilterComponent {}
|
|
|
@ -2,27 +2,11 @@ import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { SharedModule } from "../../../shared";
|
import { SharedModule } from "../../../shared";
|
||||||
|
|
||||||
import { CollectionFilterComponent } from "./collection-filter/collection-filter.component";
|
import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";
|
||||||
import { FolderFilterComponent } from "./folder-filter/folder-filter.component";
|
|
||||||
import { StatusFilterComponent } from "./status-filter/status-filter.component";
|
|
||||||
import { TypeFilterComponent } from "./type-filter/type-filter.component";
|
|
||||||
import { VaultFilterService } from "./vault-filter.service";
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule],
|
imports: [SharedModule],
|
||||||
declarations: [
|
declarations: [VaultFilterSectionComponent],
|
||||||
CollectionFilterComponent,
|
exports: [SharedModule, VaultFilterSectionComponent],
|
||||||
FolderFilterComponent,
|
|
||||||
StatusFilterComponent,
|
|
||||||
TypeFilterComponent,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
SharedModule,
|
|
||||||
CollectionFilterComponent,
|
|
||||||
FolderFilterComponent,
|
|
||||||
StatusFilterComponent,
|
|
||||||
TypeFilterComponent,
|
|
||||||
],
|
|
||||||
providers: [VaultFilterService],
|
|
||||||
})
|
})
|
||||||
export class VaultFilterSharedModule {}
|
export class VaultFilterSharedModule {}
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
import { Injectable } from "@angular/core";
|
|
||||||
import { BehaviorSubject, Observable } from "rxjs";
|
|
||||||
|
|
||||||
import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model";
|
|
||||||
import { VaultFilterService as BaseVaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
|
||||||
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 { CollectionDetailsResponse } from "@bitwarden/common/models/response/collectionResponse";
|
|
||||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class VaultFilterService extends BaseVaultFilterService {
|
|
||||||
private _collapsedFilterNodes = new BehaviorSubject<Set<string>>(null);
|
|
||||||
collapsedFilterNodes$: Observable<Set<string>> = this._collapsedFilterNodes.asObservable();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
stateService: StateService,
|
|
||||||
organizationService: OrganizationService,
|
|
||||||
folderService: FolderService,
|
|
||||||
cipherService: CipherService,
|
|
||||||
collectionService: CollectionService,
|
|
||||||
policyService: PolicyService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
protected apiService: ApiService
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
stateService,
|
|
||||||
organizationService,
|
|
||||||
folderService,
|
|
||||||
cipherService,
|
|
||||||
collectionService,
|
|
||||||
policyService
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildCollapsedFilterNodes(): Promise<Set<string>> {
|
|
||||||
const nodes = await super.buildCollapsedFilterNodes();
|
|
||||||
this._collapsedFilterNodes.next(nodes);
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async storeCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
|
|
||||||
await super.storeCollapsedFilterNodes(collapsedFilterNodes);
|
|
||||||
this._collapsedFilterNodes.next(collapsedFilterNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureVaultFiltersAreExpanded() {
|
|
||||||
const collapsedFilterNodes = await super.buildCollapsedFilterNodes();
|
|
||||||
if (!collapsedFilterNodes.has("vaults")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
collapsedFilterNodes.delete("vaults");
|
|
||||||
await this.storeCollapsedFilterNodes(collapsedFilterNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildAdminCollections(organizationId: string) {
|
|
||||||
let result: CollectionView[] = [];
|
|
||||||
const collectionResponse = await this.apiService.getCollections(organizationId);
|
|
||||||
if (collectionResponse?.data != null && collectionResponse.data.length) {
|
|
||||||
const collectionDomains = collectionResponse.data.map(
|
|
||||||
(r: CollectionDetailsResponse) => new Collection(new CollectionData(r))
|
|
||||||
);
|
|
||||||
result = await this.collectionService.decryptMany(collectionDomains);
|
|
||||||
}
|
|
||||||
|
|
||||||
const noneCollection = new CollectionView();
|
|
||||||
noneCollection.name = this.i18nService.t("unassigned");
|
|
||||||
noneCollection.organizationId = organizationId;
|
|
||||||
result.push(noneCollection);
|
|
||||||
|
|
||||||
const nestedCollections = await this.collectionService.getAllNested(result);
|
|
||||||
return new DynamicTreeNode<CollectionView>({
|
|
||||||
fullList: result,
|
|
||||||
nestedList: nestedCollections,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
<div class="card vault-filters">
|
|
||||||
<div class="container loading-spinner" *ngIf="!isLoaded">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="isLoaded">
|
|
||||||
<div class="card-header d-flex">
|
|
||||||
{{ "filters" | i18n }}
|
|
||||||
<a
|
|
||||||
class="ml-auto"
|
|
||||||
href="https://bitwarden.com/help/searching-vault/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
placeholder="{{ (searchPlaceholder | i18n) || ('searchVault' | i18n) }}"
|
|
||||||
id="search"
|
|
||||||
class="form-control"
|
|
||||||
[(ngModel)]="searchText"
|
|
||||||
(input)="searchTextChanged()"
|
|
||||||
autocomplete="off"
|
|
||||||
appAutofocus
|
|
||||||
/>
|
|
||||||
<app-organization-filter
|
|
||||||
[hide]="hideOrganizations"
|
|
||||||
[activeFilter]="activeFilter"
|
|
||||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
|
||||||
[organizations]="organizations"
|
|
||||||
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
|
|
||||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
|
||||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
|
||||||
(onFilterChange)="applyFilter($event)"
|
|
||||||
></app-organization-filter>
|
|
||||||
<div class="filter">
|
|
||||||
<app-status-filter
|
|
||||||
[hideFavorites]="hideFavorites"
|
|
||||||
[hideTrash]="hideTrash"
|
|
||||||
[activeFilter]="activeFilter"
|
|
||||||
(onFilterChange)="applyFilter($event)"
|
|
||||||
></app-status-filter>
|
|
||||||
</div>
|
|
||||||
<div class="filter">
|
|
||||||
<app-type-filter
|
|
||||||
[activeFilter]="activeFilter"
|
|
||||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
|
||||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
|
||||||
(onFilterChange)="applyFilter($event)"
|
|
||||||
></app-type-filter>
|
|
||||||
</div>
|
|
||||||
<div class="filter">
|
|
||||||
<app-folder-filter
|
|
||||||
[hide]="hideFolders"
|
|
||||||
[activeFilter]="activeFilter"
|
|
||||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
|
||||||
[folderNodes]="folders$ | async"
|
|
||||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
|
||||||
(onFilterChange)="applyFilter($event)"
|
|
||||||
(onAddFolder)="addFolder()"
|
|
||||||
(onEditFolder)="editFolder($event)"
|
|
||||||
></app-folder-filter>
|
|
||||||
</div>
|
|
||||||
<div class="filter">
|
|
||||||
<app-collection-filter
|
|
||||||
[hide]="hideCollections"
|
|
||||||
[activeFilter]="activeFilter"
|
|
||||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
|
||||||
[collectionNodes]="collections"
|
|
||||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
|
||||||
(onFilterChange)="applyFilter($event)"
|
|
||||||
></app-collection-filter>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Component, EventEmitter, Output } from "@angular/core";
|
|
||||||
|
|
||||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/vault-filter.component";
|
|
||||||
|
|
||||||
import { VaultFilterService } from "./shared/vault-filter.service";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "./app-vault-filter",
|
|
||||||
templateUrl: "vault-filter.component.html",
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
|
||||||
export class VaultFilterComponent extends BaseVaultFilterComponent {
|
|
||||||
@Output() onSearchTextChanged = new EventEmitter<string>();
|
|
||||||
|
|
||||||
searchPlaceholder: string;
|
|
||||||
searchText = "";
|
|
||||||
|
|
||||||
constructor(protected vaultFilterService: VaultFilterService) {
|
|
||||||
// This empty constructor is required to provide the web vaultFilterService subclass to super()
|
|
||||||
// TODO: refactor this to use proper dependency injection
|
|
||||||
super(vaultFilterService);
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
await super.ngOnInit();
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
|
||||||
this.vaultFilterService.collapsedFilterNodes$.subscribe((nodes) => {
|
|
||||||
this.collapsedFilterNodes = nodes;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTextChanged() {
|
|
||||||
this.onSearchTextChanged.emit(this.searchText);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { LinkSsoComponent } from "./organization-filter/link-sso.component";
|
import { LinkSsoComponent } from "./components/link-sso.component";
|
||||||
import { OrganizationFilterComponent } from "./organization-filter/organization-filter.component";
|
import { OrganizationOptionsComponent } from "./components/organization-options.component";
|
||||||
import { OrganizationOptionsComponent } from "./organization-filter/organization-options.component";
|
import { VaultFilterComponent } from "./components/vault-filter.component";
|
||||||
|
import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service";
|
||||||
|
import { VaultFilterService } from "./services/vault-filter.service";
|
||||||
import { VaultFilterSharedModule } from "./shared/vault-filter-shared.module";
|
import { VaultFilterSharedModule } from "./shared/vault-filter-shared.module";
|
||||||
import { VaultFilterComponent } from "./vault-filter.component";
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [VaultFilterSharedModule],
|
imports: [VaultFilterSharedModule],
|
||||||
declarations: [
|
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoComponent],
|
||||||
VaultFilterComponent,
|
|
||||||
OrganizationFilterComponent,
|
|
||||||
OrganizationOptionsComponent,
|
|
||||||
LinkSsoComponent,
|
|
||||||
],
|
|
||||||
exports: [VaultFilterComponent],
|
exports: [VaultFilterComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VaultFilterServiceAbstraction,
|
||||||
|
useClass: VaultFilterService,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class VaultFilterModule {}
|
export class VaultFilterModule {}
|
||||||
|
|
|
@ -7,10 +7,10 @@
|
||||||
<app-vault-filter
|
<app-vault-filter
|
||||||
#vaultFilter
|
#vaultFilter
|
||||||
[activeFilter]="activeFilter"
|
[activeFilter]="activeFilter"
|
||||||
(onFilterChange)="applyVaultFilter($event)"
|
(activeFilterChanged)="applyVaultFilter($event)"
|
||||||
(onAddFolder)="addFolder()"
|
|
||||||
(onEditFolder)="editFolder($event.id)"
|
|
||||||
(onSearchTextChanged)="filterSearchText($event)"
|
(onSearchTextChanged)="filterSearchText($event)"
|
||||||
|
(onAddFolder)="addFolder()"
|
||||||
|
(onEditFolder)="editFolder($event)"
|
||||||
></app-vault-filter>
|
></app-vault-filter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,24 +34,20 @@
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<app-vault-bulk-actions
|
<app-vault-bulk-actions
|
||||||
[ciphersComponent]="ciphersComponent"
|
[ciphersComponent]="ciphersComponent"
|
||||||
[deleted]="activeFilter.status === 'trash'"
|
[deleted]="activeFilter.isDeleted"
|
||||||
>
|
>
|
||||||
</app-vault-bulk-actions>
|
</app-vault-bulk-actions>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
(click)="addCipher()"
|
(click)="addCipher()"
|
||||||
*ngIf="activeFilter.status !== 'trash'"
|
*ngIf="!activeFilter.isDeleted"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-callout
|
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||||
type="warning"
|
|
||||||
*ngIf="activeFilter.status === 'trash'"
|
|
||||||
icon="bwi-exclamation-triangle"
|
|
||||||
>
|
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<app-vault-ciphers
|
<app-vault-ciphers
|
||||||
|
|
|
@ -8,10 +8,10 @@ import {
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
|
||||||
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
|
@ -23,6 +23,8 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
||||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||||
|
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||||
|
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||||
|
|
||||||
import { UpdateKeyComponent } from "../settings/update-key.component";
|
import { UpdateKeyComponent } from "../settings/update-key.component";
|
||||||
|
@ -33,9 +35,10 @@ import { CiphersComponent } from "./ciphers.component";
|
||||||
import { CollectionsComponent } from "./collections.component";
|
import { CollectionsComponent } from "./collections.component";
|
||||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||||
import { ShareComponent } from "./share.component";
|
import { ShareComponent } from "./share.component";
|
||||||
import { VaultService } from "./shared/vault.service";
|
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||||
import { VaultFilterService } from "./vault-filter/shared/vault-filter.service";
|
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||||
|
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "VaultComponent";
|
const BroadcasterSubscriptionId = "VaultComponent";
|
||||||
|
|
||||||
|
@ -58,8 +61,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
|
||||||
updateKeyModalRef: ViewContainerRef;
|
updateKeyModalRef: ViewContainerRef;
|
||||||
|
|
||||||
folderId: string = null;
|
|
||||||
myVaultOnly = false;
|
|
||||||
showVerifyEmail = false;
|
showVerifyEmail = false;
|
||||||
showBrowserOutdated = false;
|
showBrowserOutdated = false;
|
||||||
showUpdateKey = false;
|
showUpdateKey = false;
|
||||||
|
@ -82,10 +83,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private vaultService: VaultService,
|
private vaultFilterService: VaultFilterService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService
|
||||||
private vaultFilterService: VaultFilterService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -104,8 +104,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
this.showPremiumCallout =
|
this.showPremiumCallout =
|
||||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||||
|
|
||||||
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
await this.vaultFilterService.reloadCollections();
|
||||||
this.filterComponent.reloadOrganizations();
|
|
||||||
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
||||||
|
|
||||||
const cipherId = getCipherIdFromParams(params);
|
const cipherId = getCipherIdFromParams(params);
|
||||||
|
@ -147,8 +146,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
case "syncCompleted":
|
case "syncCompleted":
|
||||||
if (message.successfully) {
|
if (message.successfully) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter),
|
this.vaultFilterService.reloadCollections(),
|
||||||
this.filterComponent.reloadOrganizations(),
|
|
||||||
this.ciphersComponent.load(this.ciphersComponent.filter),
|
this.ciphersComponent.load(this.ciphersComponent.filter),
|
||||||
]);
|
]);
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
|
@ -173,30 +171,57 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
async applyVaultFilter(filter: VaultFilter) {
|
||||||
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
this.activeFilter = filter;
|
||||||
this.activeFilter = vaultFilter;
|
this.ciphersComponent.showAddNew = !this.activeFilter.isDeleted;
|
||||||
await this.ciphersComponent.reload(
|
await this.ciphersComponent.reload(
|
||||||
this.activeFilter.buildFilter(),
|
this.activeFilter.buildFilter(),
|
||||||
vaultFilter.status === "trash"
|
this.activeFilter.isDeleted
|
||||||
);
|
|
||||||
this.filterComponent.searchPlaceholder = this.vaultService.calculateSearchBarLocalizationString(
|
|
||||||
this.activeFilter
|
|
||||||
);
|
);
|
||||||
this.go();
|
this.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyOrganizationFilter(orgId: string) {
|
async applyOrganizationFilter(orgId: string) {
|
||||||
if (orgId == null) {
|
if (orgId == null) {
|
||||||
this.activeFilter.resetOrganization();
|
orgId = "MyVault";
|
||||||
this.activeFilter.myVaultOnly = true;
|
|
||||||
} else {
|
|
||||||
this.activeFilter.selectedOrganizationId = orgId;
|
|
||||||
}
|
}
|
||||||
await this.vaultFilterService.ensureVaultFiltersAreExpanded();
|
const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$);
|
||||||
await this.applyVaultFilter(this.activeFilter);
|
const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode<OrganizationFilter>;
|
||||||
|
this.filterComponent.filters?.organizationFilter?.action(orgNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFolder = async (): Promise<void> => {
|
||||||
|
const [modal] = await this.modalService.openViewRef(
|
||||||
|
FolderAddEditComponent,
|
||||||
|
this.folderAddEditModalRef,
|
||||||
|
(comp) => {
|
||||||
|
comp.folderId = null;
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
|
comp.onSavedFolder.subscribe(async () => {
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
editFolder = async (folder: FolderFilter): Promise<void> => {
|
||||||
|
const [modal] = await this.modalService.openViewRef(
|
||||||
|
FolderAddEditComponent,
|
||||||
|
this.folderAddEditModalRef,
|
||||||
|
(comp) => {
|
||||||
|
comp.folderId = folder.id;
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
|
comp.onSavedFolder.subscribe(async () => {
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
|
comp.onDeletedFolder.subscribe(async () => {
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
filterSearchText(searchText: string) {
|
filterSearchText(searchText: string) {
|
||||||
this.ciphersComponent.searchText = searchText;
|
this.ciphersComponent.searchText = searchText;
|
||||||
this.ciphersComponent.search(200);
|
this.ciphersComponent.search(200);
|
||||||
|
@ -208,7 +233,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
this.messagingService.send("premiumRequired");
|
this.messagingService.send("premiumRequired");
|
||||||
return;
|
return;
|
||||||
} else if (cipher.organizationId != null) {
|
} else if (cipher.organizationId != null) {
|
||||||
const org = await this.organizationService.get(cipher.organizationId);
|
const org = this.organizationService.get(cipher.organizationId);
|
||||||
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
|
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
|
||||||
this.messagingService.send("upgradeOrganization", {
|
this.messagingService.send("upgradeOrganization", {
|
||||||
organizationId: cipher.organizationId,
|
organizationId: cipher.organizationId,
|
||||||
|
@ -271,60 +296,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFolder() {
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
|
||||||
FolderAddEditComponent,
|
|
||||||
this.folderAddEditModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.folderId = null;
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
||||||
comp.onSavedFolder.subscribe(async () => {
|
|
||||||
modal.close();
|
|
||||||
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async editFolder(folderId: string) {
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
|
||||||
FolderAddEditComponent,
|
|
||||||
this.folderAddEditModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.folderId = folderId;
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
||||||
comp.onSavedFolder.subscribe(async () => {
|
|
||||||
modal.close();
|
|
||||||
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
||||||
comp.onDeletedFolder.subscribe(async () => {
|
|
||||||
modal.close();
|
|
||||||
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addCipher() {
|
async addCipher() {
|
||||||
const component = await this.editCipher(null);
|
const component = await this.editCipher(null);
|
||||||
component.type = this.activeFilter.cipherType;
|
component.type = this.activeFilter.cipherType;
|
||||||
component.folderId = this.folderId === "none" ? null : this.folderId;
|
if (this.activeFilter.organizationId !== "MyVault") {
|
||||||
if (this.activeFilter.selectedCollectionId != null) {
|
component.organizationId = this.activeFilter.organizationId;
|
||||||
const collection = this.filterComponent.collections.fullList.filter(
|
component.collections = (
|
||||||
(c) => c.id === this.activeFilter.selectedCollectionId
|
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||||
);
|
).filter((c) => !c.readOnly && c.id != null);
|
||||||
if (collection.length > 0) {
|
|
||||||
component.organizationId = collection[0].organizationId;
|
|
||||||
component.collectionIds = [this.activeFilter.selectedCollectionId];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
const selectedColId = this.activeFilter.collectionId;
|
||||||
component.folderId = this.activeFilter.selectedFolderId;
|
if (selectedColId !== "AllCollections") {
|
||||||
}
|
component.collectionIds = [selectedColId];
|
||||||
if (this.activeFilter.selectedOrganizationId) {
|
|
||||||
component.organizationId = this.activeFilter.selectedOrganizationId;
|
|
||||||
}
|
}
|
||||||
|
component.folderId = this.activeFilter.folderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipher(cipher: CipherView) {
|
async editCipher(cipher: CipherView) {
|
||||||
|
@ -382,11 +367,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
private go(queryParams: any = null) {
|
private go(queryParams: any = null) {
|
||||||
if (queryParams == null) {
|
if (queryParams == null) {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
favorites: this.activeFilter.status === "favorites" ? true : null,
|
favorites: this.activeFilter.isFavorites || null,
|
||||||
type: this.activeFilter.cipherType,
|
type: this.activeFilter.cipherType,
|
||||||
folderId: this.activeFilter.selectedFolderId,
|
folderId: this.activeFilter.folderId,
|
||||||
collectionId: this.activeFilter.selectedCollectionId,
|
collectionId: this.activeFilter.collectionId,
|
||||||
deleted: this.activeFilter.status === "trash" ? true : null,
|
deleted: this.activeFilter.isDeleted || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SharedModule, LooseComponentsModule } from "../shared";
|
||||||
|
|
||||||
import { CiphersComponent } from "./ciphers.component";
|
import { CiphersComponent } from "./ciphers.component";
|
||||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||||
import { VaultSharedModule } from "./shared/vault-shared.module";
|
import { PipesModule } from "./pipes/pipes.module";
|
||||||
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
import { VaultRoutingModule } from "./vault-routing.module";
|
import { VaultRoutingModule } from "./vault-routing.module";
|
||||||
import { VaultComponent } from "./vault.component";
|
import { VaultComponent } from "./vault.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [VaultSharedModule, VaultRoutingModule, OrganizationBadgeModule],
|
imports: [
|
||||||
|
VaultFilterModule,
|
||||||
|
VaultRoutingModule,
|
||||||
|
OrganizationBadgeModule,
|
||||||
|
PipesModule,
|
||||||
|
SharedModule,
|
||||||
|
LooseComponentsModule,
|
||||||
|
],
|
||||||
declarations: [VaultComponent, CiphersComponent],
|
declarations: [VaultComponent, CiphersComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
})
|
})
|
||||||
|
|
|
@ -277,13 +277,31 @@
|
||||||
"searchFavorites": {
|
"searchFavorites": {
|
||||||
"message": "Search Favorites"
|
"message": "Search Favorites"
|
||||||
},
|
},
|
||||||
"searchType": {
|
"searchLogin": {
|
||||||
"message": "Search Type",
|
"message": "Search Logins",
|
||||||
"description": "Search item type"
|
"description": "Search Login type"
|
||||||
|
},
|
||||||
|
"searchCard": {
|
||||||
|
"message": "Search Cards",
|
||||||
|
"description": "Search Card type"
|
||||||
|
},
|
||||||
|
"searchIdentity": {
|
||||||
|
"message": "Search Identities",
|
||||||
|
"description": "Search Identity type"
|
||||||
|
},
|
||||||
|
"searchSecureNote": {
|
||||||
|
"message": "Search Secure Notes",
|
||||||
|
"description": "Search Secure Note type"
|
||||||
},
|
},
|
||||||
"searchVault": {
|
"searchVault": {
|
||||||
"message": "Search Vault"
|
"message": "Search Vault"
|
||||||
},
|
},
|
||||||
|
"searchMyVault": {
|
||||||
|
"message": "Search My Vault"
|
||||||
|
},
|
||||||
|
"searchOrganization": {
|
||||||
|
"message": "Search Organization"
|
||||||
|
},
|
||||||
"allItems": {
|
"allItems": {
|
||||||
"message": "All Items"
|
"message": "All Items"
|
||||||
},
|
},
|
||||||
|
|
|
@ -202,7 +202,7 @@ app-sponsored-families {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button.org-options {
|
button.filter-options-icon {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
.filter-heading {
|
.filter-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
text-transform: uppercase;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -26,20 +25,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.filter-button {
|
.filter-button {
|
||||||
&:hover,
|
h3,
|
||||||
&:focus,
|
button {
|
||||||
&.active {
|
&:hover,
|
||||||
@include themify($themes) {
|
&:focus,
|
||||||
color: themed("linkColor") !important;
|
&.active {
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("linkColor") !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.toggle-button {
|
button.toggle-button,
|
||||||
|
button.add-button {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
@include themify($themes) {
|
@include themify($themes) {
|
||||||
|
@ -137,7 +140,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-options {
|
.filter-options-icon {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,7 +164,6 @@
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-transform: uppercase;
|
|
||||||
@include themify($themes) {
|
@include themify($themes) {
|
||||||
color: themed("textMuted");
|
color: themed("textMuted");
|
||||||
}
|
}
|
||||||
|
@ -176,11 +178,6 @@
|
||||||
color: themed("textHeadingColor");
|
color: themed("textHeadingColor");
|
||||||
font-weight: themed("linkWeight");
|
font-weight: themed("linkWeight");
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&.text-muted {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.show-active {
|
.show-active {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -57,6 +57,6 @@
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"Angular.ng-template"
|
"Angular.ng-template"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||||
|
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||||
|
|
||||||
|
import { DynamicTreeNode } from "../vault/vault-filter/models/dynamic-tree-node.model";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated August 30 2022: Use new VaultFilterService with observables
|
||||||
|
*/
|
||||||
|
export abstract class DeprecatedVaultFilterService {
|
||||||
|
buildOrganizations: () => Promise<Organization[]>;
|
||||||
|
buildNestedFolders: (organizationId?: string) => Observable<DynamicTreeNode<FolderView>>;
|
||||||
|
buildCollections: (organizationId?: string) => Promise<DynamicTreeNode<CollectionView>>;
|
||||||
|
buildCollapsedFilterNodes: () => Promise<Set<string>>;
|
||||||
|
storeCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
|
||||||
|
checkForSingleOrganizationPolicy: () => Promise<boolean>;
|
||||||
|
checkForPersonalOwnershipPolicy: () => Promise<boolean>;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
import { firstValueFrom, Observable } from "rxjs";
|
import { firstValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { DeprecatedVaultFilterService } from "@bitwarden/angular/abstractions/deprecated-vault-filter.service";
|
||||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/treeNode";
|
import { ITreeNodeObject } from "@bitwarden/common/models/domain/treeNode";
|
||||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||||
|
@ -8,8 +9,9 @@ import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||||
|
|
||||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||||
import { VaultFilter } from "../models/vault-filter.model";
|
import { VaultFilter } from "../models/vault-filter.model";
|
||||||
import { VaultFilterService } from "../services/vault-filter.service";
|
|
||||||
|
|
||||||
|
// TODO: Replace with refactored web vault filter component
|
||||||
|
// and refactor desktop/browser vault filters
|
||||||
@Directive()
|
@Directive()
|
||||||
export class VaultFilterComponent implements OnInit {
|
export class VaultFilterComponent implements OnInit {
|
||||||
@Input() activeFilter: VaultFilter = new VaultFilter();
|
@Input() activeFilter: VaultFilter = new VaultFilter();
|
||||||
|
@ -31,7 +33,7 @@ export class VaultFilterComponent implements OnInit {
|
||||||
collections: DynamicTreeNode<CollectionView>;
|
collections: DynamicTreeNode<CollectionView>;
|
||||||
folders$: Observable<DynamicTreeNode<FolderView>>;
|
folders$: Observable<DynamicTreeNode<FolderView>>;
|
||||||
|
|
||||||
constructor(protected vaultFilterService: VaultFilterService) {}
|
constructor(protected vaultFilterService: DeprecatedVaultFilterService) {}
|
||||||
|
|
||||||
get displayCollections() {
|
get displayCollections() {
|
||||||
return this.collections?.fullList != null && this.collections.fullList.length > 0;
|
return this.collections?.fullList != null && this.collections.fullList.length > 0;
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
|
||||||
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
|
||||||
|
|
||||||
export class DynamicTreeNode<T extends CollectionView | FolderView> {
|
export class DynamicTreeNode<T extends ITreeNodeObject> {
|
||||||
fullList: T[];
|
fullList: T[];
|
||||||
nestedList: TreeNode<T>[];
|
nestedList: TreeNode<T>[];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { firstValueFrom, from, mergeMap, Observable } from "rxjs";
|
import { firstValueFrom, mergeMap, Observable, from } from "rxjs";
|
||||||
|
|
||||||
|
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/abstractions/deprecated-vault-filter.service";
|
||||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||||
|
@ -19,7 +20,7 @@ import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||||
const NestingDelimiter = "/";
|
const NestingDelimiter = "/";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VaultFilterService {
|
export class VaultFilterService implements DeprecatedVaultFilterServiceAbstraction {
|
||||||
constructor(
|
constructor(
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
|
@ -106,6 +107,6 @@ export class VaultFilterService {
|
||||||
const folders = await this.getAllFoldersNested(
|
const folders = await this.getAllFoldersNested(
|
||||||
await firstValueFrom(this.folderService.folderViews$)
|
await firstValueFrom(this.folderService.folderViews$)
|
||||||
);
|
);
|
||||||
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
|
return ServiceUtils.getTreeNodeObjectFromList(folders, id) as TreeNode<FolderView>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import { ITreeNodeObject, TreeNode } from "../models/domain/treeNode";
|
import { ITreeNodeObject, TreeNode } from "../models/domain/treeNode";
|
||||||
|
|
||||||
export class ServiceUtils {
|
export class ServiceUtils {
|
||||||
|
/**
|
||||||
|
* Recursively adds a node to nodeTree
|
||||||
|
* @param {TreeNode<ITreeNodeObject>[]} nodeTree - An array of TreeNodes that the node will be added to
|
||||||
|
* @param {number} partIndex - Index of the `parts` array that is being processed
|
||||||
|
* @param {string[]} parts - Array of strings that represent the path to the `obj` node
|
||||||
|
* @param {ITreeNodeObject} obj - The node to be added to the tree
|
||||||
|
* @param {ITreeNodeObject} parent - The parent node of the `obj` node
|
||||||
|
* @param {string} delimiter - The delimiter used to split the path string
|
||||||
|
*/
|
||||||
static nestedTraverse(
|
static nestedTraverse(
|
||||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||||
partIndex: number,
|
partIndex: number,
|
||||||
|
@ -22,7 +31,7 @@ export class ServiceUtils {
|
||||||
}
|
}
|
||||||
if (end && nodeTree[i].node.id !== obj.id) {
|
if (end && nodeTree[i].node.id !== obj.id) {
|
||||||
// Another node with the same name.
|
// Another node with the same name.
|
||||||
nodeTree.push(new TreeNode(obj, partName, parent));
|
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ServiceUtils.nestedTraverse(
|
ServiceUtils.nestedTraverse(
|
||||||
|
@ -38,7 +47,7 @@ export class ServiceUtils {
|
||||||
|
|
||||||
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
|
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
|
||||||
if (end) {
|
if (end) {
|
||||||
nodeTree.push(new TreeNode(obj, partName, parent));
|
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newPartName = parts[partIndex] + delimiter + parts[partIndex + 1];
|
const newPartName = parts[partIndex] + delimiter + parts[partIndex + 1];
|
||||||
|
@ -53,7 +62,37 @@ 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 {string} id - The id of the node to be found
|
||||||
|
* @returns {TreeNode<ITreeNodeObject>} The node with a matching `id`
|
||||||
|
*/
|
||||||
static getTreeNodeObject(
|
static getTreeNodeObject(
|
||||||
|
nodeTree: TreeNode<ITreeNodeObject>,
|
||||||
|
id: string
|
||||||
|
): TreeNode<ITreeNodeObject> {
|
||||||
|
if (nodeTree.node.id === id) {
|
||||||
|
return nodeTree;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < nodeTree.children.length; i++) {
|
||||||
|
if (nodeTree.children[i].children != null) {
|
||||||
|
const node = ServiceUtils.getTreeNodeObject(nodeTree.children[i], id);
|
||||||
|
if (node !== null) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches an array of tree nodes for a node with a matching `id`
|
||||||
|
* @param {TreeNode<ITreeNodeObject>} nodeTree - An array of TreeNode branches that will be searched
|
||||||
|
* @param {string} id - The id of the node to be found
|
||||||
|
* @returns {TreeNode<ITreeNodeObject>} The node with a matching `id`
|
||||||
|
*/
|
||||||
|
static getTreeNodeObjectFromList(
|
||||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||||
id: string
|
id: string
|
||||||
): TreeNode<ITreeNodeObject> {
|
): TreeNode<ITreeNodeObject> {
|
||||||
|
@ -61,7 +100,7 @@ export class ServiceUtils {
|
||||||
if (nodeTree[i].node.id === id) {
|
if (nodeTree[i].node.id === id) {
|
||||||
return nodeTree[i];
|
return nodeTree[i];
|
||||||
} else if (nodeTree[i].children != null) {
|
} else if (nodeTree[i].children != null) {
|
||||||
const node = ServiceUtils.getTreeNodeObject(nodeTree[i].children, id);
|
const node = ServiceUtils.getTreeNodeObjectFromList(nodeTree[i].children, id);
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,6 +137,10 @@ export class Organization {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canUseAdminCollections() {
|
||||||
|
return this.canEditAnyCollection;
|
||||||
|
}
|
||||||
|
|
||||||
get canDeleteAnyCollection() {
|
get canDeleteAnyCollection() {
|
||||||
return (
|
return (
|
||||||
this.isAdmin ||
|
this.isAdmin ||
|
||||||
|
|
|
@ -3,10 +3,15 @@ export class TreeNode<T extends ITreeNodeObject> {
|
||||||
node: T;
|
node: T;
|
||||||
children: TreeNode<T>[] = [];
|
children: TreeNode<T>[] = [];
|
||||||
|
|
||||||
constructor(node: T, name: string, parent: T) {
|
constructor(node: T, parent: T, name?: string, id?: string) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.node.name = name;
|
if (name) {
|
||||||
|
this.node.name = name;
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
this.node.id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,10 @@ export class CollectionService implements CollectionServiceAbstraction {
|
||||||
return decryptedCollections;
|
return decryptedCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated August 30 2022: Moved to new Vault Filter Service
|
||||||
|
* Remove when Desktop and Browser are updated
|
||||||
|
*/
|
||||||
async getAllNested(collections: CollectionView[] = null): Promise<TreeNode<CollectionView>[]> {
|
async getAllNested(collections: CollectionView[] = null): Promise<TreeNode<CollectionView>[]> {
|
||||||
if (collections == null) {
|
if (collections == null) {
|
||||||
collections = await this.getAllDecrypted();
|
collections = await this.getAllDecrypted();
|
||||||
|
@ -106,9 +110,13 @@ export class CollectionService implements CollectionServiceAbstraction {
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated August 30 2022: Moved to new Vault Filter Service
|
||||||
|
* Remove when Desktop and Browser are updated
|
||||||
|
*/
|
||||||
async getNested(id: string): Promise<TreeNode<CollectionView>> {
|
async getNested(id: string): Promise<TreeNode<CollectionView>> {
|
||||||
const collections = await this.getAllNested();
|
const collections = await this.getAllNested();
|
||||||
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionView>;
|
return ServiceUtils.getTreeNodeObjectFromList(collections, id) as TreeNode<CollectionView>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsert(collection: CollectionData | CollectionData[]): Promise<any> {
|
async upsert(collection: CollectionData | CollectionData[]): Promise<any> {
|
||||||
|
|
Loading…
Reference in New Issue