[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 { BrowserModule } from "@angular/platform-browser";
|
||||
|
||||
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/abstractions/deprecated-vault-filter.service";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||
|
||||
|
@ -22,6 +23,11 @@ import { VaultFilterComponent } from "./vault-filter.component";
|
|||
TypeFilterComponent,
|
||||
],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [VaultFilterService],
|
||||
providers: [
|
||||
{
|
||||
provide: DeprecatedVaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
},
|
||||
],
|
||||
})
|
||||
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 { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../vault/vault-filter/vault-filter.component";
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../vault/vault-filter/components/vault-filter.component";
|
||||
import {
|
||||
VaultFilterList,
|
||||
VaultFilterType,
|
||||
} from "../../../vault/vault-filter/shared/models/vault-filter-section.type";
|
||||
import { CollectionFilter } from "../../../vault/vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-vault-filter",
|
||||
templateUrl: "../../../vault/vault-filter/vault-filter.component.html",
|
||||
templateUrl: "../../../vault/vault-filter/components/vault-filter.component.html",
|
||||
})
|
||||
export class VaultFilterComponent extends BaseVaultFilterComponent {
|
||||
hideOrganizations = true;
|
||||
hideFavorites = true;
|
||||
hideFolders = true;
|
||||
|
||||
organization: Organization;
|
||||
|
||||
async initCollections() {
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
return await this.vaultFilterService.buildAdminCollections(this.organization.id);
|
||||
export class VaultFilterComponent extends BaseVaultFilterComponent implements OnInit, OnDestroy {
|
||||
@Input() set organization(value: Organization) {
|
||||
if (value && value !== this._organization) {
|
||||
this._organization = value;
|
||||
this.vaultFilterService.setOrganizationFilter(this._organization);
|
||||
}
|
||||
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() {
|
||||
this.collections = await this.initCollections();
|
||||
ngOnDestroy() {
|
||||
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 { VaultFilterService as VaultFilterServiceAbstraction } from "../../../vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilterSharedModule } from "../../../vault/vault-filter/shared/vault-filter-shared.module";
|
||||
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultFilterSharedModule],
|
||||
declarations: [VaultFilterComponent],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultFilterModule {}
|
||||
|
|
|
@ -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">
|
||||
<app-organization-vault-filter
|
||||
#vaultFilter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
(onFilterChange)="applyVaultFilter($event)"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
|
@ -32,7 +33,7 @@
|
|||
<div class="ml-auto d-flex">
|
||||
<app-vault-bulk-actions
|
||||
[ciphersComponent]="ciphersComponent"
|
||||
[deleted]="deleted"
|
||||
[deleted]="activeFilter.isDeleted"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-vault-bulk-actions>
|
||||
|
@ -40,13 +41,17 @@
|
|||
type="button"
|
||||
class="btn btn-outline-primary btn-sm ml-auto"
|
||||
(click)="addCipher()"
|
||||
*ngIf="!deleted"
|
||||
*ngIf="!activeFilter.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-callout type="warning" *ngIf="deleted" icon="bwi bwi-exclamation-triangle">
|
||||
<app-callout
|
||||
type="warning"
|
||||
*ngIf="activeFilter.isDeleted"
|
||||
icon="bwi bwi-exclamation-triangle"
|
||||
>
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
<app-org-vault-ciphers
|
||||
|
|
|
@ -8,10 +8,10 @@ import {
|
|||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
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 { CipherService } from "@bitwarden/common/abstractions/cipher.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
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 { 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 { AddEditComponent } from "./add-edit.component";
|
||||
|
@ -53,19 +53,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
eventsModalRef: ViewContainerRef;
|
||||
|
||||
organization: Organization;
|
||||
collectionId: string = null;
|
||||
type: CipherType = null;
|
||||
trashCleanupWarning: string = null;
|
||||
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(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private router: Router,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private syncService: SyncService,
|
||||
|
@ -75,7 +69,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private vaultService: VaultService,
|
||||
private cipherService: CipherService,
|
||||
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
|
||||
this.route.parent.params.subscribe(async (params: any) => {
|
||||
this.organization = await this.organizationService.get(params.organizationId);
|
||||
this.vaultFilterComponent.organization = this.organization;
|
||||
this.organization = this.organizationService.get(params.organizationId);
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
|
||||
/* 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":
|
||||
if (message.successfully) {
|
||||
await Promise.all([
|
||||
this.vaultFilterComponent.reloadCollectionsAndFolders(),
|
||||
this.vaultFilterService.reloadCollections(),
|
||||
this.ciphersComponent.refresh(),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
|
@ -114,12 +106,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.firstLoaded) {
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders();
|
||||
}
|
||||
this.firstLoaded = true;
|
||||
|
||||
await this.ciphersComponent.reload();
|
||||
await this.ciphersComponent.reload(
|
||||
this.activeFilter.buildFilter(),
|
||||
this.activeFilter.isDeleted
|
||||
);
|
||||
|
||||
if (qParams.viewEvents != null) {
|
||||
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
|
||||
|
@ -134,7 +124,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
if (cipherId) {
|
||||
if (
|
||||
// Handle users with implicit collection access since they use the admin endpoint
|
||||
this.organization.canEditAnyCollection ||
|
||||
this.organization.canUseAdminCollections ||
|
||||
(await this.cipherService.get(cipherId)) != null
|
||||
) {
|
||||
this.editCipherId(cipherId);
|
||||
|
@ -155,23 +145,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
get deleted(): boolean {
|
||||
return this.activeFilter.status === "trash";
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
||||
this.activeFilter = vaultFilter;
|
||||
async applyVaultFilter(filter: VaultFilter) {
|
||||
this.activeFilter = filter;
|
||||
this.ciphersComponent.showAddNew = !this.activeFilter.isDeleted;
|
||||
await this.ciphersComponent.reload(
|
||||
this.activeFilter.buildFilter(),
|
||||
vaultFilter.status === "trash"
|
||||
this.activeFilter.isDeleted
|
||||
);
|
||||
this.vaultFilterComponent.searchPlaceholder =
|
||||
this.vaultService.calculateSearchBarLocalizationString(this.activeFilter);
|
||||
this.go();
|
||||
}
|
||||
|
||||
|
@ -211,16 +195,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async editCipherCollections(cipher: CipherView) {
|
||||
const currCollections = await firstValueFrom(this.vaultFilterService.filteredCollections$);
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
CollectionsComponent,
|
||||
this.collectionsModalRef,
|
||||
(comp) => {
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
comp.collectionIds = cipher.collectionIds;
|
||||
comp.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||
(c) => !c.readOnly && c.id != null
|
||||
);
|
||||
}
|
||||
comp.collections = currCollections.filter((c) => !c.readOnly && c.id != null);
|
||||
comp.organization = this.organization;
|
||||
comp.cipherId = cipher.id;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
|
@ -235,14 +216,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
async addCipher() {
|
||||
const component = await this.editCipher(null);
|
||||
component.organizationId = this.organization.id;
|
||||
component.type = this.type;
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
component.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||
(c) => !c.readOnly && c.id != null
|
||||
);
|
||||
}
|
||||
if (this.collectionId != null) {
|
||||
component.collectionIds = [this.collectionId];
|
||||
component.type = this.activeFilter.cipherType;
|
||||
component.collections = (
|
||||
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||
).filter((c) => !c.readOnly && c.id != null);
|
||||
if (this.activeFilter.collectionId) {
|
||||
component.collectionIds = [this.activeFilter.collectionId];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,13 +273,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
const component = await this.editCipher(cipher);
|
||||
component.cloneMode = true;
|
||||
component.organizationId = this.organization.id;
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
component.collections = this.vaultFilterComponent.collections.fullList.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.collections = (
|
||||
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||
).filter((c) => !c.readOnly && c.id != null);
|
||||
component.collectionIds = cipher.collectionIds;
|
||||
}
|
||||
|
||||
|
@ -318,8 +293,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
type: this.activeFilter.cipherType,
|
||||
collectionId: this.activeFilter.selectedCollectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
collectionId: this.activeFilter.collectionId,
|
||||
deleted: this.activeFilter.isDeleted || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
|
@ -8,7 +9,7 @@ import { VaultRoutingModule } from "./vault-routing.module";
|
|||
import { VaultComponent } from "./vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultSharedModule, VaultRoutingModule, VaultFilterModule],
|
||||
imports: [VaultRoutingModule, VaultFilterModule, SharedModule, LooseComponentsModule],
|
||||
declarations: [VaultComponent, CiphersComponent],
|
||||
exports: [VaultComponent],
|
||||
})
|
||||
|
|
|
@ -126,20 +126,13 @@ import { CollectionsComponent } from "../vault/collections.component";
|
|||
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
|
||||
import { OrganizationBadgeModule } from "../vault/organization-badge/organization-badge.module";
|
||||
import { ShareComponent } from "../vault/share.component";
|
||||
import { VaultFilterModule } from "../vault/vault-filter/vault-filter.module";
|
||||
|
||||
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.
|
||||
// If you are building new functionality, please create or extend a feature module instead.
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
VaultFilterModule,
|
||||
OrganizationBadgeModule,
|
||||
OrganizationCreateModule,
|
||||
RegisterFormModule,
|
||||
],
|
||||
imports: [SharedModule, OrganizationBadgeModule, OrganizationCreateModule, RegisterFormModule],
|
||||
declarations: [
|
||||
PremiumBadgeComponent,
|
||||
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 { 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 { 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({
|
||||
selector: "app-organization-options",
|
||||
|
@ -24,9 +26,8 @@ export class OrganizationOptionsComponent {
|
|||
policies: Policy[];
|
||||
loaded = false;
|
||||
|
||||
@Input() organization: Organization;
|
||||
|
||||
constructor(
|
||||
@Inject(OptionsInput) private organization: OrganizationFilter,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
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 { CollectionFilterComponent } from "./collection-filter/collection-filter.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";
|
||||
import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [
|
||||
CollectionFilterComponent,
|
||||
FolderFilterComponent,
|
||||
StatusFilterComponent,
|
||||
TypeFilterComponent,
|
||||
],
|
||||
exports: [
|
||||
SharedModule,
|
||||
CollectionFilterComponent,
|
||||
FolderFilterComponent,
|
||||
StatusFilterComponent,
|
||||
TypeFilterComponent,
|
||||
],
|
||||
providers: [VaultFilterService],
|
||||
declarations: [VaultFilterSectionComponent],
|
||||
exports: [SharedModule, VaultFilterSectionComponent],
|
||||
})
|
||||
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 { LinkSsoComponent } from "./organization-filter/link-sso.component";
|
||||
import { OrganizationFilterComponent } from "./organization-filter/organization-filter.component";
|
||||
import { OrganizationOptionsComponent } from "./organization-filter/organization-options.component";
|
||||
import { LinkSsoComponent } from "./components/link-sso.component";
|
||||
import { OrganizationOptionsComponent } from "./components/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 { VaultFilterComponent } from "./vault-filter.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultFilterSharedModule],
|
||||
declarations: [
|
||||
VaultFilterComponent,
|
||||
OrganizationFilterComponent,
|
||||
OrganizationOptionsComponent,
|
||||
LinkSsoComponent,
|
||||
],
|
||||
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoComponent],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultFilterModule {}
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
<app-vault-filter
|
||||
#vaultFilter
|
||||
[activeFilter]="activeFilter"
|
||||
(onFilterChange)="applyVaultFilter($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event.id)"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event)"
|
||||
></app-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,24 +34,20 @@
|
|||
<div class="ml-auto d-flex">
|
||||
<app-vault-bulk-actions
|
||||
[ciphersComponent]="ciphersComponent"
|
||||
[deleted]="activeFilter.status === 'trash'"
|
||||
[deleted]="activeFilter.isDeleted"
|
||||
>
|
||||
</app-vault-bulk-actions>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
(click)="addCipher()"
|
||||
*ngIf="activeFilter.status !== 'trash'"
|
||||
*ngIf="!activeFilter.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-callout
|
||||
type="warning"
|
||||
*ngIf="activeFilter.status === 'trash'"
|
||||
icon="bwi-exclamation-triangle"
|
||||
>
|
||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
<app-vault-ciphers
|
||||
|
|
|
@ -8,10 +8,10 @@ import {
|
|||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
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 { CipherService } from "@bitwarden/common/abstractions/cipher.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 { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
|
||||
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 { UpdateKeyComponent } from "../settings/update-key.component";
|
||||
|
@ -33,9 +35,10 @@ import { CiphersComponent } from "./ciphers.component";
|
|||
import { CollectionsComponent } from "./collections.component";
|
||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
import { VaultService } from "./shared/vault.service";
|
||||
import { VaultFilterService } from "./vault-filter/shared/vault-filter.service";
|
||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
|
||||
|
@ -58,8 +61,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
updateKeyModalRef: ViewContainerRef;
|
||||
|
||||
folderId: string = null;
|
||||
myVaultOnly = false;
|
||||
showVerifyEmail = false;
|
||||
showBrowserOutdated = false;
|
||||
showUpdateKey = false;
|
||||
|
@ -82,10 +83,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
private ngZone: NgZone,
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private vaultService: VaultService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private cipherService: CipherService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private vaultFilterService: VaultFilterService
|
||||
private passwordRepromptService: PasswordRepromptService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -104,8 +104,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
this.showPremiumCallout =
|
||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
this.filterComponent.reloadOrganizations();
|
||||
await this.vaultFilterService.reloadCollections();
|
||||
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
||||
|
||||
const cipherId = getCipherIdFromParams(params);
|
||||
|
@ -147,8 +146,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
await Promise.all([
|
||||
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter),
|
||||
this.filterComponent.reloadOrganizations(),
|
||||
this.vaultFilterService.reloadCollections(),
|
||||
this.ciphersComponent.load(this.ciphersComponent.filter),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
|
@ -173,30 +171,57 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
||||
this.activeFilter = vaultFilter;
|
||||
async applyVaultFilter(filter: VaultFilter) {
|
||||
this.activeFilter = filter;
|
||||
this.ciphersComponent.showAddNew = !this.activeFilter.isDeleted;
|
||||
await this.ciphersComponent.reload(
|
||||
this.activeFilter.buildFilter(),
|
||||
vaultFilter.status === "trash"
|
||||
);
|
||||
this.filterComponent.searchPlaceholder = this.vaultService.calculateSearchBarLocalizationString(
|
||||
this.activeFilter
|
||||
this.activeFilter.isDeleted
|
||||
);
|
||||
this.go();
|
||||
}
|
||||
|
||||
async applyOrganizationFilter(orgId: string) {
|
||||
if (orgId == null) {
|
||||
this.activeFilter.resetOrganization();
|
||||
this.activeFilter.myVaultOnly = true;
|
||||
} else {
|
||||
this.activeFilter.selectedOrganizationId = orgId;
|
||||
orgId = "MyVault";
|
||||
}
|
||||
await this.vaultFilterService.ensureVaultFiltersAreExpanded();
|
||||
await this.applyVaultFilter(this.activeFilter);
|
||||
const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$);
|
||||
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) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
|
@ -208,7 +233,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
this.messagingService.send("premiumRequired");
|
||||
return;
|
||||
} 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)) {
|
||||
this.messagingService.send("upgradeOrganization", {
|
||||
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() {
|
||||
const component = await this.editCipher(null);
|
||||
component.type = this.activeFilter.cipherType;
|
||||
component.folderId = this.folderId === "none" ? null : this.folderId;
|
||||
if (this.activeFilter.selectedCollectionId != null) {
|
||||
const collection = this.filterComponent.collections.fullList.filter(
|
||||
(c) => c.id === this.activeFilter.selectedCollectionId
|
||||
);
|
||||
if (collection.length > 0) {
|
||||
component.organizationId = collection[0].organizationId;
|
||||
component.collectionIds = [this.activeFilter.selectedCollectionId];
|
||||
if (this.activeFilter.organizationId !== "MyVault") {
|
||||
component.organizationId = this.activeFilter.organizationId;
|
||||
component.collections = (
|
||||
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||
).filter((c) => !c.readOnly && c.id != null);
|
||||
}
|
||||
const selectedColId = this.activeFilter.collectionId;
|
||||
if (selectedColId !== "AllCollections") {
|
||||
component.collectionIds = [selectedColId];
|
||||
}
|
||||
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
||||
component.folderId = this.activeFilter.selectedFolderId;
|
||||
}
|
||||
if (this.activeFilter.selectedOrganizationId) {
|
||||
component.organizationId = this.activeFilter.selectedOrganizationId;
|
||||
}
|
||||
component.folderId = this.activeFilter.folderId;
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView) {
|
||||
|
@ -382,11 +367,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
private go(queryParams: any = null) {
|
||||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
favorites: this.activeFilter.status === "favorites" ? true : null,
|
||||
favorites: this.activeFilter.isFavorites || null,
|
||||
type: this.activeFilter.cipherType,
|
||||
folderId: this.activeFilter.selectedFolderId,
|
||||
collectionId: this.activeFilter.selectedCollectionId,
|
||||
deleted: this.activeFilter.status === "trash" ? true : null,
|
||||
folderId: this.activeFilter.folderId,
|
||||
collectionId: this.activeFilter.collectionId,
|
||||
deleted: this.activeFilter.isDeleted || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule, LooseComponentsModule } from "../shared";
|
||||
|
||||
import { CiphersComponent } from "./ciphers.component";
|
||||
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 { VaultComponent } from "./vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultSharedModule, VaultRoutingModule, OrganizationBadgeModule],
|
||||
imports: [
|
||||
VaultFilterModule,
|
||||
VaultRoutingModule,
|
||||
OrganizationBadgeModule,
|
||||
PipesModule,
|
||||
SharedModule,
|
||||
LooseComponentsModule,
|
||||
],
|
||||
declarations: [VaultComponent, CiphersComponent],
|
||||
exports: [VaultComponent],
|
||||
})
|
||||
|
|
|
@ -277,13 +277,31 @@
|
|||
"searchFavorites": {
|
||||
"message": "Search Favorites"
|
||||
},
|
||||
"searchType": {
|
||||
"message": "Search Type",
|
||||
"description": "Search item type"
|
||||
"searchLogin": {
|
||||
"message": "Search Logins",
|
||||
"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": {
|
||||
"message": "Search Vault"
|
||||
},
|
||||
"searchMyVault": {
|
||||
"message": "Search My Vault"
|
||||
},
|
||||
"searchOrganization": {
|
||||
"message": "Search Organization"
|
||||
},
|
||||
"allItems": {
|
||||
"message": "All Items"
|
||||
},
|
||||
|
|
|
@ -202,7 +202,7 @@ app-sponsored-families {
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
button.org-options {
|
||||
button.filter-options-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
.filter-heading {
|
||||
display: flex;
|
||||
text-transform: uppercase;
|
||||
align-items: center;
|
||||
|
||||
* {
|
||||
|
@ -26,7 +25,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
button.filter-button {
|
||||
.filter-button {
|
||||
h3,
|
||||
button {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.active {
|
||||
|
@ -38,8 +39,10 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.toggle-button {
|
||||
button.toggle-button,
|
||||
button.add-button {
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include themify($themes) {
|
||||
|
@ -137,7 +140,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.org-options {
|
||||
.filter-options-icon {
|
||||
padding: 0 2px;
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +164,6 @@
|
|||
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
@include themify($themes) {
|
||||
color: themed("textMuted");
|
||||
}
|
||||
|
@ -176,11 +178,6 @@
|
|||
color: themed("textHeadingColor");
|
||||
font-weight: themed("linkWeight");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.text-muted {
|
||||
}
|
||||
}
|
||||
}
|
||||
.show-active {
|
||||
display: none;
|
||||
|
|
|
@ -57,6 +57,6 @@
|
|||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"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 { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { DeprecatedVaultFilterService } from "@bitwarden/angular/abstractions/deprecated-vault-filter.service";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/treeNode";
|
||||
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 { 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()
|
||||
export class VaultFilterComponent implements OnInit {
|
||||
@Input() activeFilter: VaultFilter = new VaultFilter();
|
||||
|
@ -31,7 +33,7 @@ export class VaultFilterComponent implements OnInit {
|
|||
collections: DynamicTreeNode<CollectionView>;
|
||||
folders$: Observable<DynamicTreeNode<FolderView>>;
|
||||
|
||||
constructor(protected vaultFilterService: VaultFilterService) {}
|
||||
constructor(protected vaultFilterService: DeprecatedVaultFilterService) {}
|
||||
|
||||
get displayCollections() {
|
||||
return this.collections?.fullList != null && this.collections.fullList.length > 0;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
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 { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||
|
||||
export class DynamicTreeNode<T extends CollectionView | FolderView> {
|
||||
export class DynamicTreeNode<T extends ITreeNodeObject> {
|
||||
fullList: T[];
|
||||
nestedList: TreeNode<T>[];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
|
@ -19,7 +20,7 @@ import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
|||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService {
|
||||
export class VaultFilterService implements DeprecatedVaultFilterServiceAbstraction {
|
||||
constructor(
|
||||
protected stateService: StateService,
|
||||
protected organizationService: OrganizationService,
|
||||
|
@ -106,6 +107,6 @@ export class VaultFilterService {
|
|||
const folders = await this.getAllFoldersNested(
|
||||
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";
|
||||
|
||||
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(
|
||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||
partIndex: number,
|
||||
|
@ -22,7 +31,7 @@ export class ServiceUtils {
|
|||
}
|
||||
if (end && nodeTree[i].node.id !== obj.id) {
|
||||
// Another node with the same name.
|
||||
nodeTree.push(new TreeNode(obj, partName, parent));
|
||||
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||
return;
|
||||
}
|
||||
ServiceUtils.nestedTraverse(
|
||||
|
@ -38,7 +47,7 @@ export class ServiceUtils {
|
|||
|
||||
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
|
||||
if (end) {
|
||||
nodeTree.push(new TreeNode(obj, partName, parent));
|
||||
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||
return;
|
||||
}
|
||||
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(
|
||||
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>[],
|
||||
id: string
|
||||
): TreeNode<ITreeNodeObject> {
|
||||
|
@ -61,7 +100,7 @@ export class ServiceUtils {
|
|||
if (nodeTree[i].node.id === id) {
|
||||
return nodeTree[i];
|
||||
} 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) {
|
||||
return node;
|
||||
}
|
||||
|
|
|
@ -137,6 +137,10 @@ export class Organization {
|
|||
);
|
||||
}
|
||||
|
||||
get canUseAdminCollections() {
|
||||
return this.canEditAnyCollection;
|
||||
}
|
||||
|
||||
get canDeleteAnyCollection() {
|
||||
return (
|
||||
this.isAdmin ||
|
||||
|
|
|
@ -3,11 +3,16 @@ export class TreeNode<T extends ITreeNodeObject> {
|
|||
node: T;
|
||||
children: TreeNode<T>[] = [];
|
||||
|
||||
constructor(node: T, name: string, parent: T) {
|
||||
constructor(node: T, parent: T, name?: string, id?: string) {
|
||||
this.parent = parent;
|
||||
this.node = node;
|
||||
if (name) {
|
||||
this.node.name = name;
|
||||
}
|
||||
if (id) {
|
||||
this.node.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITreeNodeObject {
|
||||
|
|
|
@ -91,6 +91,10 @@ export class CollectionService implements CollectionServiceAbstraction {
|
|||
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>[]> {
|
||||
if (collections == null) {
|
||||
collections = await this.getAllDecrypted();
|
||||
|
@ -106,9 +110,13 @@ export class CollectionService implements CollectionServiceAbstraction {
|
|||
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>> {
|
||||
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> {
|
||||
|
|
Loading…
Reference in New Issue