mirror of
https://github.com/bitwarden/browser
synced 2026-01-21 11:53:34 +00:00
Desktop/pm 18769/migrate vault filters (#17919)
Migrated vault filters to new v3 vault's navigation * Decoupled existing vault filtering from vault component by using routed params with routed-vault-filter-bridge * Converted vault filters to standalone components * Removed extending filter Base Components from deprecated /libs/angular library and handled logic directly * Moved shared 'models' and 'services' directories from web-vault into /libs/vault
This commit is contained in:
@@ -7,9 +7,9 @@ import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable, switchMap, of, map } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -3,8 +3,9 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
@@ -3,12 +3,13 @@ import { TestBed, discardPeriodicTasks, fakeAsync, tick } from "@angular/core/te
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skipWhile } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import * as vaultFilterSvc from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
@@ -14,17 +14,17 @@ import {
|
||||
take,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model";
|
||||
import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CollectionWithIdExport } from "@bitwarden/common/models/export/collection-with-id.export";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
import { SelectionReadOnly } from "../selection-read-only";
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { filter, firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService, CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
CollectionService,
|
||||
CollectionData,
|
||||
Collection,
|
||||
CollectionDetailsResponse as ApiCollectionDetailsResponse,
|
||||
CollectionResponse as ApiCollectionResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import * as inquirer from "inquirer";
|
||||
import * as JSZip from "jszip";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<app-side-nav slot="side-nav">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n" />
|
||||
|
||||
<bit-nav-item icon="bwi-vault" [text]="'vault' | i18n" route="new-vault" />
|
||||
<app-vault-filter />
|
||||
<app-send-filters-nav />
|
||||
|
||||
<bit-nav-item icon="bwi-generate" [text]="'generator' | i18n" (click)="openGenerator()" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { DialogService, NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component";
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
import { DesktopLayoutComponent } from "./desktop-layout.component";
|
||||
@@ -20,6 +21,13 @@ import { DesktopLayoutComponent } from "./desktop-layout.component";
|
||||
})
|
||||
class MockSendFiltersNavComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
template: "",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockVaultFiltersNavComponent {}
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
@@ -59,8 +67,8 @@ describe("DesktopLayoutComponent", () => {
|
||||
],
|
||||
})
|
||||
.overrideComponent(DesktopLayoutComponent, {
|
||||
remove: { imports: [SendFiltersNavComponent] },
|
||||
add: { imports: [MockSendFiltersNavComponent] },
|
||||
remove: { imports: [SendFiltersNavComponent, VaultFilterComponent] },
|
||||
add: { imports: [MockSendFiltersNavComponent, MockVaultFiltersNavComponent] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
@@ -93,4 +101,11 @@ describe("DesktopLayoutComponent", () => {
|
||||
|
||||
expect(sendFiltersNav).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders vault filters navigation component", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const vaultFiltersNav = compiled.querySelector("app-vault-filter");
|
||||
|
||||
expect(vaultFiltersNav).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PasswordManagerLogo } from "@bitwarden/assets/svg";
|
||||
import { DialogService, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component";
|
||||
import { ExportDesktopComponent } from "../tools/export/export-desktop.component";
|
||||
import { CredentialGeneratorComponent } from "../tools/generator/credential-generator.component";
|
||||
import { ImportDesktopComponent } from "../tools/import/import-desktop.component";
|
||||
@@ -22,6 +23,7 @@ import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
DesktopSideNavComponent,
|
||||
VaultFilterComponent,
|
||||
SendFiltersNavComponent,
|
||||
],
|
||||
templateUrl: "./desktop-layout.component.html",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { APP_INITIALIZER, NgModule } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, merge } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import {
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
PolicyService as PolicyServiceAbstraction,
|
||||
InternalPolicyService,
|
||||
@@ -107,6 +108,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic
|
||||
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
@@ -122,7 +124,15 @@ import {
|
||||
SessionTimeoutSettingsComponentService,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
DefaultSshImportPromptService,
|
||||
SshImportPromptService,
|
||||
VaultFilterServiceAbstraction,
|
||||
VaultFilterService,
|
||||
RoutedVaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
VAULT_FILTER_BASE_ROUTE,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopAuthRequestAnsweringService } from "../../auth/services/auth-request-answering/desktop-auth-request-answering.service";
|
||||
@@ -508,6 +518,34 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: SessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction, SessionTimeoutTypeService, PolicyServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
deps: [
|
||||
OrganizationService,
|
||||
FolderService,
|
||||
CipherServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
StateProvider,
|
||||
CollectionService,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VAULT_FILTER_BASE_ROUTE,
|
||||
useValue: "/new-vault",
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RoutedVaultFilterService,
|
||||
useClass: RoutedVaultFilterService,
|
||||
deps: [ActivatedRoute],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RoutedVaultFilterBridgeService,
|
||||
useClass: RoutedVaultFilterBridgeService,
|
||||
deps: [Router, RoutedVaultFilterService, VaultFilterServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestAnsweringService,
|
||||
useClass: DesktopAuthRequestAnsweringService,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
@if (collection().children.length) {
|
||||
<bit-nav-group
|
||||
[icon]="collection().node.icon"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (childCollection of collection().children; track childCollection.node.id) {
|
||||
<app-collection-filter [collection]="childCollection" [activeFilter]="activeFilter()" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
} @else {
|
||||
<bit-nav-item
|
||||
[icon]="collection().node.icon"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
/>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, input, computed } from "@angular/core";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { VaultFilter, CollectionFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-collection-filter",
|
||||
templateUrl: "collection-filter.component.html",
|
||||
imports: [A11yTitleDirective, NavigationModule],
|
||||
})
|
||||
export class CollectionFilterComponent {
|
||||
protected readonly collection = input<TreeNode<CollectionFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
|
||||
protected readonly displayName = computed<string>(() => {
|
||||
return this.collection().node.name;
|
||||
});
|
||||
|
||||
protected readonly isActive = computed<boolean>(() => {
|
||||
return (
|
||||
this.collection().node.id === this.activeFilter()?.collectionId &&
|
||||
!!this.activeFilter()?.selectedCollectionNode
|
||||
);
|
||||
});
|
||||
|
||||
protected applyFilter(event: Event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedCollectionNode = this.collection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
@if (folder().children.length) {
|
||||
<bit-nav-group
|
||||
[icon]="folder().node.icon"
|
||||
[class.active]="isActive()"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@if (folder()?.node.id) {
|
||||
<button
|
||||
type="button"
|
||||
slot="end"
|
||||
class="edit-button"
|
||||
bitIconButton="bwi-pencil"
|
||||
buttonType="nav-contrast"
|
||||
size="small"
|
||||
[label]="'editFolder' | i18n"
|
||||
(click)="editFolder(folder().node)"
|
||||
></button>
|
||||
}
|
||||
@for (childFolder of folder().children; track childFolder.node.id) {
|
||||
<app-folder-filter [folder]="childFolder" [activeFilter]="activeFilter()" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
} @else {
|
||||
<bit-nav-item
|
||||
[icon]="folder().node.icon"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
>
|
||||
@if (folder()?.node.id) {
|
||||
<button
|
||||
type="button"
|
||||
slot="end"
|
||||
class="edit-button"
|
||||
bitIconButton="bwi-pencil"
|
||||
buttonType="nav-contrast"
|
||||
size="small"
|
||||
[label]="'editFolder' | i18n"
|
||||
(click)="editFolder(folder().node)"
|
||||
></button>
|
||||
}
|
||||
</bit-nav-item>
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component, input, computed, output } from "@angular/core";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { IconButtonModule, NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultFilter, FolderFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-folder-filter",
|
||||
templateUrl: "folder-filter.component.html",
|
||||
imports: [A11yTitleDirective, NavigationModule, IconButtonModule, I18nPipe],
|
||||
})
|
||||
export class FolderFilterComponent {
|
||||
protected readonly folder = input<TreeNode<FolderFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
protected onEditFolder = output<FolderFilter>();
|
||||
|
||||
protected readonly displayName = computed<string>(() => {
|
||||
return this.folder().node.name;
|
||||
});
|
||||
|
||||
protected readonly isActive = computed<boolean>(() => {
|
||||
return (
|
||||
this.folder().node.id === this.activeFilter()?.folderId &&
|
||||
!!this.activeFilter()?.selectedFolderNode
|
||||
);
|
||||
});
|
||||
|
||||
protected applyFilter(event: Event) {
|
||||
event.stopPropagation();
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedFolderNode = this.folder();
|
||||
}
|
||||
}
|
||||
|
||||
protected editFolder(folder: FolderFilter) {
|
||||
this.onEditFolder.emit(folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@if (show()) {
|
||||
<bit-nav-group
|
||||
icon="bwi-filter"
|
||||
(click)="applyAllVaultsFilter()"
|
||||
[text]="'allVaults' | i18n"
|
||||
[attr.aria-pressed]="!activeFilter()?.selectedOrganizationNode"
|
||||
[appA11yTitle]="'allVaults' | i18n"
|
||||
variant="tree"
|
||||
[open]="true"
|
||||
[forceActiveStyles]="!activeFilter()?.selectedOrganizationNode"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (organization of organizations().children ?? []; track organization.node.id) {
|
||||
<bit-nav-item
|
||||
[icon]="organization.node.icon"
|
||||
[forceActiveStyles]="organization.node.id === activeFilter()?.organizationId"
|
||||
[text]="organization.node.name"
|
||||
[appA11yTitle]="organization.node.name"
|
||||
(click)="applyFilter($event, organization)"
|
||||
/>
|
||||
@if (!organization.node.enabled) {
|
||||
<span class="tw-ml-auto">
|
||||
<i
|
||||
class="bwi bwi-fw bwi-exclamation-triangle text-danger mr-auto"
|
||||
[attr.aria-label]="'organizationIsDisabled' | i18n"
|
||||
[appA11yTitle]="'organizationIsDisabled' | i18n"
|
||||
></i>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</bit-nav-group>
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component, computed, input, inject } from "@angular/core";
|
||||
|
||||
import { DisplayMode } from "@bitwarden/angular/vault/vault-filter/models/display-mode";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ToastService, NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { OrganizationFilter, VaultFilter, VaultFilterServiceAbstraction } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-organization-filter",
|
||||
templateUrl: "organization-filter.component.html",
|
||||
imports: [A11yTitleDirective, NavigationModule, I18nPipe],
|
||||
})
|
||||
export class OrganizationFilterComponent {
|
||||
private toastService: ToastService = inject(ToastService);
|
||||
private i18nService: I18nService = inject(I18nService);
|
||||
private vaultFilterService: VaultFilterServiceAbstraction = inject(VaultFilterServiceAbstraction);
|
||||
|
||||
protected readonly hide = input(false);
|
||||
protected readonly organizations = input<TreeNode<OrganizationFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
protected readonly activeOrganizationDataOwnership = input<boolean>(false);
|
||||
protected readonly activeSingleOrganizationPolicy = input<boolean>(false);
|
||||
|
||||
protected readonly show = computed(() => {
|
||||
const hiddenDisplayModes: DisplayMode[] = [
|
||||
"singleOrganizationAndOrganizatonDataOwnershipPolicies",
|
||||
];
|
||||
return (
|
||||
!this.hide() &&
|
||||
this.organizations()?.children.length > 0 &&
|
||||
hiddenDisplayModes.indexOf(this.displayMode()) === -1
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly displayMode = computed<DisplayMode>(() => {
|
||||
let displayMode: DisplayMode = "organizationMember";
|
||||
if (this.organizations() == null || this.organizations().children.length < 1) {
|
||||
displayMode = "noOrganizations";
|
||||
} else if (this.activeOrganizationDataOwnership() && !this.activeSingleOrganizationPolicy()) {
|
||||
displayMode = "organizationDataOwnershipPolicy";
|
||||
} else if (!this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) {
|
||||
displayMode = "singleOrganizationPolicy";
|
||||
} else if (this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) {
|
||||
displayMode = "singleOrganizationAndOrganizatonDataOwnershipPolicies";
|
||||
}
|
||||
|
||||
return displayMode;
|
||||
});
|
||||
|
||||
protected applyFilter(event: Event, organization: TreeNode<OrganizationFilter>) {
|
||||
event.stopPropagation();
|
||||
if (!organization.node.enabled) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("disabledOrganizationFilterError"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.vaultFilterService.setOrganizationFilter(organization.node);
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedOrganizationNode = organization;
|
||||
}
|
||||
}
|
||||
|
||||
protected applyAllVaultsFilter() {
|
||||
this.vaultFilterService.clearOrganizationFilter();
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedOrganizationNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@if (!hideArchive()) {
|
||||
<bit-nav-item
|
||||
[icon]="archiveFilter.icon"
|
||||
[forceActiveStyles]="activeFilter()?.isArchived"
|
||||
(click)="handleArchiveFilter($event)"
|
||||
[text]="archiveFilter.name | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.isArchived"
|
||||
[appA11yTitle]="archiveFilter.name | i18n"
|
||||
/>
|
||||
@if (!(canArchive$ | async)) {
|
||||
<app-premium-badge />
|
||||
}
|
||||
}
|
||||
<bit-nav-item
|
||||
[icon]="trashFilter.icon"
|
||||
[forceActiveStyles]="activeFilter()?.isDeleted"
|
||||
(click)="applyFilter('trash')"
|
||||
[text]="trashFilter.name | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.isDeleted"
|
||||
[appA11yTitle]="trashFilter.name | i18n"
|
||||
/>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, viewChild, input, inject } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultFilter, CipherStatus, CipherTypeFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-status-filter",
|
||||
templateUrl: "status-filter.component.html",
|
||||
imports: [CommonModule, A11yTitleDirective, NavigationModule, PremiumBadgeComponent, I18nPipe],
|
||||
})
|
||||
export class StatusFilterComponent {
|
||||
private accountService: AccountService = inject(AccountService);
|
||||
private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService);
|
||||
|
||||
protected readonly hideArchive = input(false);
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
protected readonly archiveFilter: CipherTypeFilter = {
|
||||
id: "archive",
|
||||
name: "archiveNoun",
|
||||
type: "archive",
|
||||
icon: "bwi-archive",
|
||||
};
|
||||
protected readonly trashFilter: CipherTypeFilter = {
|
||||
id: "trash",
|
||||
name: "trash",
|
||||
type: "trash",
|
||||
icon: "bwi-trash",
|
||||
};
|
||||
|
||||
protected applyFilter(filterType: CipherStatus) {
|
||||
let filter: CipherTypeFilter = null;
|
||||
if (filterType === "archive") {
|
||||
filter = this.archiveFilter;
|
||||
} else if (filterType === "trash") {
|
||||
filter = this.trashFilter;
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
this.activeFilter().selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(filter, null);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly premiumBadgeComponent = viewChild.required(PremiumBadgeComponent);
|
||||
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
protected canArchive$ = this.userId$.pipe(
|
||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||
);
|
||||
|
||||
protected hasArchivedCiphers$ = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)),
|
||||
),
|
||||
);
|
||||
|
||||
protected async handleArchiveFilter(event: Event) {
|
||||
const [canArchive, hasArchivedCiphers] = await firstValueFrom(
|
||||
combineLatest([this.canArchive$, this.hasArchivedCiphers$]),
|
||||
);
|
||||
|
||||
if (canArchive || hasArchivedCiphers) {
|
||||
this.applyFilter("archive");
|
||||
} else {
|
||||
await this.premiumBadgeComponent().promptForPremium(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<bit-nav-group
|
||||
icon="bwi-filter"
|
||||
(click)="applyAllItemsFilter($event)"
|
||||
[text]="'allItems' | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.selectedCipherTypeNode?.node?.id === 'AllItems'"
|
||||
[appA11yTitle]="'allItems' | i18n"
|
||||
variant="tree"
|
||||
[open]="true"
|
||||
[forceActiveStyles]="activeFilter()?.selectedCipherTypeNode?.node?.id === 'AllItems'"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (typeFilter of typeFilters$ | async; track typeFilter) {
|
||||
<bit-nav-item
|
||||
[icon]="typeFilter.node.icon"
|
||||
[forceActiveStyles]="
|
||||
activeFilter()?.selectedCipherTypeNode.node.type === typeFilter.node.type
|
||||
"
|
||||
(click)="applyTypeFilter($event, typeFilter)"
|
||||
[text]="typeFilter.node.name"
|
||||
[attr.aria-pressed]="
|
||||
activeFilter()?.selectedCipherTypeNode.node.type === typeFilter.node.type
|
||||
"
|
||||
[appA11yTitle]="typeFilter.node.name"
|
||||
/>
|
||||
}
|
||||
</bit-nav-group>
|
||||
@@ -0,0 +1,57 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, input, inject } from "@angular/core";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultFilter, CipherTypeFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-type-filter",
|
||||
templateUrl: "type-filter.component.html",
|
||||
imports: [CommonModule, A11yTitleDirective, NavigationModule, I18nPipe],
|
||||
})
|
||||
export class TypeFilterComponent {
|
||||
private restrictedItemTypesService: RestrictedItemTypesService = inject(
|
||||
RestrictedItemTypesService,
|
||||
);
|
||||
|
||||
protected readonly cipherTypes = input<TreeNode<CipherTypeFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
|
||||
protected applyTypeFilter(event: Event, cipherType: TreeNode<CipherTypeFilter>) {
|
||||
event.stopPropagation();
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedCipherTypeNode = cipherType;
|
||||
}
|
||||
}
|
||||
|
||||
protected applyAllItemsFilter(event: Event) {
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedCipherTypeNode = this.cipherTypes();
|
||||
}
|
||||
}
|
||||
|
||||
protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types from the typeFilters array
|
||||
this.cipherTypes().children.filter(
|
||||
(type) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.allowViewOrgIds.length === 0 &&
|
||||
restrictedType.cipherType === type.node.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
@if (!isLoaded) {
|
||||
<div class="container loading-spinner">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
} @else {
|
||||
<bit-nav-group icon="bwi-vault" [text]="'vault' | i18n" route="new-vault">
|
||||
<app-organization-filter
|
||||
[activeFilter]="activeFilter()"
|
||||
[organizations]="organizations$ | async"
|
||||
[activeOrganizationDataOwnership]="activeOrganizationDataOwnershipPolicy"
|
||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||
/>
|
||||
<app-type-filter [activeFilter]="activeFilter()" [cipherTypes]="cipherTypes$ | async" />
|
||||
<app-status-filter [hideArchive]="!showArchiveVaultFilter" [activeFilter]="activeFilter()" />
|
||||
@if (showCollectionsFilter()) {
|
||||
<bit-nav-group
|
||||
icon="bwi-collection"
|
||||
[text]="'collections' | i18n"
|
||||
variant="tree"
|
||||
[appA11yTitle]="'collections' | i18n"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (collection of (collections$ | async)?.children ?? []; track collection.node.id) {
|
||||
<app-collection-filter [activeFilter]="activeFilter()" [collection]="collection" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
}
|
||||
<bit-nav-group
|
||||
icon="bwi-folder"
|
||||
[text]="'folders' | i18n"
|
||||
variant="tree"
|
||||
[appA11yTitle]="'folders' | i18n"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (folder of (folders$ | async)?.children ?? []; track folder.node.id) {
|
||||
<app-folder-filter
|
||||
[activeFilter]="activeFilter()"
|
||||
[folder]="folder"
|
||||
(onEditFolder)="editFolder($event)"
|
||||
/>
|
||||
}
|
||||
</bit-nav-group>
|
||||
</bit-nav-group>
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject, OnInit, output, computed, signal } from "@angular/core";
|
||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule, DialogService, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
OrganizationFilter,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
VaultFilter,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
AddEditFolderDialogComponent,
|
||||
RoutedVaultFilterBridgeService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../../services/desktop-premium-upgrade-prompt.service";
|
||||
|
||||
import { CollectionFilterComponent } from "./filters/collection-filter.component";
|
||||
import { FolderFilterComponent } from "./filters/folder-filter.component";
|
||||
import { OrganizationFilterComponent } from "./filters/organization-filter.component";
|
||||
import { StatusFilterComponent } from "./filters/status-filter.component";
|
||||
import { TypeFilterComponent } from "./filters/type-filter.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
imports: [
|
||||
I18nPipe,
|
||||
NavigationModule,
|
||||
CommonModule,
|
||||
OrganizationFilterComponent,
|
||||
StatusFilterComponent,
|
||||
TypeFilterComponent,
|
||||
CollectionFilterComponent,
|
||||
FolderFilterComponent,
|
||||
A11yTitleDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: PremiumUpgradePromptService,
|
||||
useClass: DesktopPremiumUpgradePromptService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultFilterComponent implements OnInit {
|
||||
private routedVaultFilterBridgeService = inject(RoutedVaultFilterBridgeService);
|
||||
private vaultFilterService: VaultFilterService = inject(VaultFilterService);
|
||||
private accountService: AccountService = inject(AccountService);
|
||||
private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService);
|
||||
private folderService: FolderService = inject(FolderService);
|
||||
private policyService: PolicyService = inject(PolicyService);
|
||||
private dialogService: DialogService = inject(DialogService);
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
protected readonly activeFilter = signal<VaultFilter | null>(null);
|
||||
protected onFilterChange = output<VaultFilter>();
|
||||
|
||||
private activeUserId: UserId;
|
||||
protected isLoaded = false;
|
||||
protected showArchiveVaultFilter = false;
|
||||
protected activeOrganizationDataOwnershipPolicy: boolean;
|
||||
protected activeSingleOrganizationPolicy: boolean;
|
||||
protected organizations$: Observable<TreeNode<OrganizationFilter>>;
|
||||
protected collections$: Observable<TreeNode<CollectionFilter>>;
|
||||
protected folders$: Observable<TreeNode<FolderFilter>>;
|
||||
protected cipherTypes$: Observable<TreeNode<CipherTypeFilter>>;
|
||||
|
||||
protected readonly showCollectionsFilter = computed<boolean>(() => {
|
||||
return this.organizations$ != null && !this.activeFilter()?.isMyVaultSelected;
|
||||
});
|
||||
|
||||
private async setActivePolicies() {
|
||||
this.activeOrganizationDataOwnershipPolicy = await firstValueFrom(
|
||||
this.policyService.policyAppliesToUser$(
|
||||
PolicyType.OrganizationDataOwnership,
|
||||
this.activeUserId,
|
||||
),
|
||||
);
|
||||
this.activeSingleOrganizationPolicy = await firstValueFrom(
|
||||
this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, this.activeUserId),
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organizations$ = this.vaultFilterService.organizationTree$;
|
||||
if (
|
||||
this.organizations$ != null &&
|
||||
(await firstValueFrom(this.organizations$)).children.length > 0
|
||||
) {
|
||||
await this.setActivePolicies();
|
||||
}
|
||||
this.cipherTypes$ = this.vaultFilterService.cipherTypeTree$;
|
||||
this.folders$ = this.vaultFilterService.folderTree$;
|
||||
this.collections$ = this.vaultFilterService.collectionTree$;
|
||||
|
||||
this.showArchiveVaultFilter = await firstValueFrom(
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||
);
|
||||
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((filter) => {
|
||||
this.activeFilter.set(filter);
|
||||
});
|
||||
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
protected async editFolder(folder: FolderFilter) {
|
||||
if (!this.activeUserId) {
|
||||
return;
|
||||
}
|
||||
const folderView = await firstValueFrom(
|
||||
this.folderService.getDecrypted$(folder.id, this.activeUserId),
|
||||
);
|
||||
|
||||
if (!folderView) {
|
||||
return;
|
||||
}
|
||||
|
||||
AddEditFolderDialogComponent.open(this.dialogService, {
|
||||
editFolderConfig: {
|
||||
folder: {
|
||||
...folderView,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,32 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observable } from "rxjs";
|
||||
import {
|
||||
firstValueFrom,
|
||||
Subject,
|
||||
takeUntil,
|
||||
switchMap,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
from,
|
||||
} from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -60,8 +58,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
ChangeLoginPasswordService,
|
||||
@@ -78,6 +74,9 @@ import {
|
||||
PasswordRepromptService,
|
||||
CipherFormComponent,
|
||||
ArchiveCipherUtilitiesService,
|
||||
VaultFilter,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
@@ -86,8 +85,6 @@ import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-pr
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { AssignCollectionsDesktopComponent } from "../vault/assign-collections";
|
||||
import { ItemFooterComponent } from "../vault/item-footer.component";
|
||||
import { VaultFilterComponent } from "../vault/vault-filter/vault-filter.component";
|
||||
import { VaultFilterModule } from "../vault/vault-filter/vault-filter.module";
|
||||
import { VaultItemsV2Component } from "../vault/vault-items-v2.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
@@ -107,7 +104,6 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
ItemModule,
|
||||
ButtonModule,
|
||||
PremiumBadgeComponent,
|
||||
VaultFilterModule,
|
||||
VaultItemsV2Component,
|
||||
],
|
||||
providers: [
|
||||
@@ -134,21 +130,11 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultComponent<C extends CipherViewLike>
|
||||
implements OnInit, OnDestroy, CopyClickListener
|
||||
{
|
||||
export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(VaultItemsV2Component, { static: true })
|
||||
vaultItemsComponent: VaultItemsV2Component<C> | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(VaultFilterComponent, { static: true })
|
||||
vaultFilterComponent: VaultFilterComponent | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||
folderAddEditModalRef: ViewContainerRef | null = null;
|
||||
vaultItemsComponent: VaultItemsV2Component<CipherView> | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CipherFormComponent)
|
||||
@@ -194,6 +180,7 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
private allOrganizations: Organization[] = [];
|
||||
private allCollections: CollectionView[] = [];
|
||||
private filteredCollections: CollectionView[] = [];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -209,7 +196,6 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
private totpService: TotpService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private searchBarService: SearchBarService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
@@ -220,11 +206,12 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
private folderService: FolderService,
|
||||
private configService: ConfigService,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private policyService: PolicyService,
|
||||
private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService,
|
||||
private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -240,6 +227,14 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
this.userHasPremiumAccess = canAccessPremium;
|
||||
});
|
||||
|
||||
// Subscribe to filter changes from router params via the bridge service
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(
|
||||
switchMap((vaultFilter: VaultFilter) => from(this.applyVaultFilter(vaultFilter))),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone
|
||||
.run(async () => {
|
||||
@@ -267,15 +262,7 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
break;
|
||||
case "syncCompleted":
|
||||
if (this.vaultItemsComponent) {
|
||||
await this.vaultItemsComponent
|
||||
.reload(this.activeFilter.buildFilter())
|
||||
.catch(() => {});
|
||||
}
|
||||
if (this.vaultFilterComponent) {
|
||||
await this.vaultFilterComponent
|
||||
.reloadCollectionsAndFolders(this.activeFilter)
|
||||
.catch(() => {});
|
||||
await this.vaultFilterComponent.reloadOrganizations().catch(() => {});
|
||||
await this.vaultItemsComponent.refresh().catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "modalShown":
|
||||
@@ -377,6 +364,12 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
.subscribe((collections) => {
|
||||
this.allCollections = collections;
|
||||
});
|
||||
|
||||
this.vaultFilterService.filteredCollections$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((collections) => {
|
||||
this.filteredCollections = collections;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -403,19 +396,6 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
this.addType = paramCipherAddType;
|
||||
await this.addCipher(this.addType).catch(() => {});
|
||||
}
|
||||
|
||||
const paramCipherType = toCipherType(params.type);
|
||||
this.activeFilter = new VaultFilter({
|
||||
status: params.deleted ? "trash" : params.favorites ? "favorites" : "all",
|
||||
cipherType: params.action === "add" || paramCipherType == null ? undefined : paramCipherType,
|
||||
selectedFolderId: params.folderId,
|
||||
selectedCollectionId: params.selectedCollectionId,
|
||||
selectedOrganizationId: params.selectedOrganizationId,
|
||||
myVaultOnly: params.myVaultOnly ?? false,
|
||||
});
|
||||
if (this.vaultItemsComponent) {
|
||||
await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,9 +419,7 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
this.collections =
|
||||
this.vaultFilterComponent?.collections?.fullList.filter((c) =>
|
||||
cipher.collectionIds.includes(c.id),
|
||||
) ?? null;
|
||||
this.filteredCollections?.filter((c) => cipher.collectionIds.includes(c.id)) ?? null;
|
||||
this.action = "view";
|
||||
|
||||
await this.go().catch(() => {});
|
||||
@@ -798,19 +776,45 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a filter function to handle CipherListView objects.
|
||||
* CipherListView has a different type structure where type can be a string or object.
|
||||
* This wrapper converts it to CipherView-compatible structure before filtering.
|
||||
*/
|
||||
private wrapFilterForCipherListView(
|
||||
filterFn: (cipher: CipherView) => boolean,
|
||||
): (cipher: CipherViewLike) => boolean {
|
||||
return (cipher: CipherViewLike) => {
|
||||
// For CipherListView, create a proxy object with the correct type property
|
||||
if (CipherViewLikeUtils.isCipherListView(cipher)) {
|
||||
const proxyCipher = {
|
||||
...cipher,
|
||||
type: CipherViewLikeUtils.getType(cipher),
|
||||
// Normalize undefined organizationId to null for filter compatibility
|
||||
organizationId: cipher.organizationId ?? null,
|
||||
// Explicitly include isDeleted and isArchived since they might be getters
|
||||
isDeleted: CipherViewLikeUtils.isDeleted(cipher),
|
||||
isArchived: CipherViewLikeUtils.isArchived(cipher),
|
||||
};
|
||||
return filterFn(proxyCipher as any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.searchBarService.setPlaceholderText(
|
||||
this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)),
|
||||
);
|
||||
this.activeFilter = vaultFilter;
|
||||
await this.vaultItemsComponent
|
||||
?.reload(
|
||||
this.activeFilter.buildFilter(),
|
||||
vaultFilter.status === "trash",
|
||||
vaultFilter.status === "archive",
|
||||
)
|
||||
.catch(() => {});
|
||||
await this.go().catch(() => {});
|
||||
|
||||
const originalFilterFn = this.activeFilter.buildFilter();
|
||||
const wrappedFilterFn = this.wrapFilterForCipherListView(originalFilterFn);
|
||||
|
||||
await this.vaultItemsComponent?.reload(
|
||||
wrappedFilterFn,
|
||||
vaultFilter.isDeleted,
|
||||
vaultFilter.isArchived,
|
||||
);
|
||||
}
|
||||
|
||||
private getAvailableCollections(cipher: CipherView): CollectionView[] {
|
||||
@@ -824,25 +828,25 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
}
|
||||
|
||||
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||
if (vaultFilter.status === "favorites") {
|
||||
if (vaultFilter.isFavorites) {
|
||||
return "searchFavorites";
|
||||
}
|
||||
if (vaultFilter.status === "trash") {
|
||||
if (vaultFilter.isDeleted) {
|
||||
return "searchTrash";
|
||||
}
|
||||
if (vaultFilter.cipherType != null) {
|
||||
return "searchType";
|
||||
}
|
||||
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") {
|
||||
if (vaultFilter.folderId != null && vaultFilter.folderId !== "none") {
|
||||
return "searchFolder";
|
||||
}
|
||||
if (vaultFilter.selectedCollectionId != null) {
|
||||
if (vaultFilter.collectionId != null) {
|
||||
return "searchCollection";
|
||||
}
|
||||
if (vaultFilter.selectedOrganizationId != null) {
|
||||
if (vaultFilter.organizationId != null) {
|
||||
return "searchOrganization";
|
||||
}
|
||||
if (vaultFilter.myVaultOnly) {
|
||||
if (vaultFilter.isMyVaultSelected) {
|
||||
return "searchMyVault";
|
||||
}
|
||||
return "searchVault";
|
||||
@@ -863,23 +867,6 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
if (!folderView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {
|
||||
editFolderConfig: {
|
||||
folder: {
|
||||
...folderView,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (
|
||||
result === AddEditFolderDialogResult.Deleted ||
|
||||
result === AddEditFolderDialogResult.Created
|
||||
) {
|
||||
await this.vaultFilterComponent?.reloadCollectionsAndFolders(this.activeFilter);
|
||||
}
|
||||
}
|
||||
|
||||
/** Refresh the current cipher object */
|
||||
@@ -919,19 +906,13 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
queryParams = {
|
||||
action: this.action,
|
||||
cipherId: this.cipherId,
|
||||
favorites: this.favorites ? true : null,
|
||||
type: this.type,
|
||||
folderId: this.folderId,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
organizationId: this.organizationId,
|
||||
myVaultOnly: this.myVaultOnly,
|
||||
};
|
||||
}
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -966,21 +947,23 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
}
|
||||
|
||||
private prefillCipherFromFilter() {
|
||||
if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) {
|
||||
const collections = this.vaultFilterComponent.collections?.fullList.filter(
|
||||
(c) => c.id === this.activeFilter.selectedCollectionId,
|
||||
if (this.activeFilter.collectionId != null) {
|
||||
const collections = this.filteredCollections?.filter(
|
||||
(c) => c.id === this.activeFilter.collectionId,
|
||||
);
|
||||
if (collections.length > 0) {
|
||||
if (collections?.length > 0) {
|
||||
this.addOrganizationId = collections[0].organizationId;
|
||||
this.addCollectionIds = [this.activeFilter.selectedCollectionId];
|
||||
this.addCollectionIds = [this.activeFilter.collectionId];
|
||||
}
|
||||
} else if (this.activeFilter.selectedOrganizationId) {
|
||||
this.addOrganizationId = this.activeFilter.selectedOrganizationId;
|
||||
} else if (this.activeFilter.organizationId) {
|
||||
this.addOrganizationId = this.activeFilter.organizationId;
|
||||
} else {
|
||||
// clear out organizationId when the user switches to a personal vault filter
|
||||
this.addOrganizationId = null;
|
||||
}
|
||||
this.folderId = this.activeFilter.selectedFolderId;
|
||||
if (this.activeFilter.folderId && this.activeFilter.selectedFolderNode) {
|
||||
this.folderId = this.activeFilter.folderId;
|
||||
}
|
||||
|
||||
if (this.config == null) {
|
||||
return;
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
@@ -32,6 +32,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
|
||||
@@ -7,12 +7,12 @@ import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
import {
|
||||
CollectionAdminService,
|
||||
OrganizationUserApiService,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { CollectionId } from "@bitwarden/sdk-internal";
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./utils";
|
||||
export * from "./collection-badge";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
@Pipe({
|
||||
name: "collectionNameFromId",
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils";
|
||||
|
||||
describe("CollectionUtils Service", () => {
|
||||
describe("getNestedCollectionTree", () => {
|
||||
it("should return collections properly sorted if provided out of order", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
const childCollection = new CollectionView({
|
||||
name: "Parent/Child",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
collections.push(childCollection);
|
||||
collections.push(parentCollection);
|
||||
|
||||
// Act
|
||||
const result = getNestedCollectionTree(collections);
|
||||
|
||||
// Assert
|
||||
expect(result[0].node.name).toBe("Parent");
|
||||
expect(result[0].children[0].node.name).toBe("Child");
|
||||
});
|
||||
|
||||
it("should return an empty array if no collections are provided", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
// Act
|
||||
const result = getNestedCollectionTree(collections);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlatCollectionTree", () => {
|
||||
it("should flatten a tree node with no children", () => {
|
||||
// Arrange
|
||||
const collection = new CollectionView({
|
||||
name: "Test Collection",
|
||||
id: "test-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [
|
||||
new TreeNode<CollectionView>(collection, {} as TreeNode<CollectionView>),
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe(collection);
|
||||
});
|
||||
|
||||
it("should flatten a tree node with children", () => {
|
||||
// Arrange
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
id: "parent-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child1Collection = new CollectionView({
|
||||
name: "Child 1",
|
||||
id: "child1-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child2Collection = new CollectionView({
|
||||
name: "Child 2",
|
||||
id: "child2-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const grandchildCollection = new CollectionView({
|
||||
name: "Grandchild",
|
||||
id: "grandchild-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const parentNode = new TreeNode<CollectionView>(
|
||||
parentCollection,
|
||||
{} as TreeNode<CollectionView>,
|
||||
);
|
||||
const child1Node = new TreeNode<CollectionView>(child1Collection, parentNode);
|
||||
const child2Node = new TreeNode<CollectionView>(child2Collection, parentNode);
|
||||
const grandchildNode = new TreeNode<CollectionView>(grandchildCollection, child1Node);
|
||||
|
||||
parentNode.children = [child1Node, child2Node];
|
||||
child1Node.children = [grandchildNode];
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [parentNode];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(4);
|
||||
expect(result[0]).toBe(parentCollection);
|
||||
expect(result).toContain(child1Collection);
|
||||
expect(result).toContain(child2Collection);
|
||||
expect(result).toContain(grandchildCollection);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
CollectionAdminView,
|
||||
CollectionView,
|
||||
NestingDelimiter,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
|
||||
export function getNestedCollectionTree(
|
||||
collections: CollectionAdminView[],
|
||||
): TreeNode<CollectionAdminView>[];
|
||||
export function getNestedCollectionTree(collections: CollectionView[]): TreeNode<CollectionView>[];
|
||||
export function getNestedCollectionTree(
|
||||
collections: (CollectionView | CollectionAdminView)[],
|
||||
): TreeNode<CollectionView | CollectionAdminView>[] {
|
||||
if (!collections) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
|
||||
// modifies the names of collections.
|
||||
// These changes risk affecting collections store in StateService.
|
||||
const clonedCollections: CollectionView[] | CollectionAdminView[] = collections
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(cloneCollection);
|
||||
|
||||
const all: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
const groupedByOrg = new Map<OrganizationId, (CollectionView | CollectionAdminView)[]>();
|
||||
clonedCollections.map((c) => {
|
||||
const key = c.organizationId;
|
||||
(groupedByOrg.get(key) ?? groupedByOrg.set(key, []).get(key)!).push(c);
|
||||
});
|
||||
for (const group of groupedByOrg.values()) {
|
||||
const nodes: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
for (const c of group) {
|
||||
const parts = c.name ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, c, undefined, NestingDelimiter);
|
||||
}
|
||||
all.push(...nodes);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
export function cloneCollection(collection: CollectionView): CollectionView;
|
||||
export function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
export function cloneCollection(
|
||||
collection: CollectionView | CollectionAdminView,
|
||||
): CollectionView | CollectionAdminView {
|
||||
let cloned;
|
||||
|
||||
if (collection instanceof CollectionAdminView) {
|
||||
cloned = Object.assign(
|
||||
new CollectionAdminView({ ...collection, name: collection.name }),
|
||||
collection,
|
||||
);
|
||||
} else {
|
||||
cloned = Object.assign(
|
||||
new CollectionView({ ...collection, name: collection.name }),
|
||||
collection,
|
||||
);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
export function getFlatCollectionTree(nodes: TreeNode<CollectionView>[]): CollectionView[];
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionView | CollectionAdminView>[],
|
||||
): (CollectionView | CollectionAdminView)[] {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return nodes.flatMap((node) => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return [node.node];
|
||||
}
|
||||
|
||||
const children = getFlatCollectionTree(node.children);
|
||||
return [node.node, ...children];
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./collection-utils";
|
||||
@@ -15,15 +15,15 @@ import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstraction
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import {
|
||||
VaultFilterService,
|
||||
VaultFilterList,
|
||||
VaultFilterSection,
|
||||
VaultFilterType,
|
||||
} from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter-section.type";
|
||||
import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
CollectionFilter,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SearchModule } from "@bitwarden/components";
|
||||
import { VaultFilterServiceAbstraction } from "@bitwarden/vault";
|
||||
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilterSharedModule } from "../../../../vault/individual-vault/vault-filter/shared/vault-filter-shared.module";
|
||||
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { map, Observable, ReplaySubject, Subject } from "rxjs";
|
||||
|
||||
import { CollectionAdminView, CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { VaultFilterService as BaseVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/vault-filter.service";
|
||||
import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
import { VaultFilterService as BaseVaultFilterService, CollectionFilter } from "@bitwarden/vault";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService extends BaseVaultFilterService implements OnDestroy {
|
||||
|
||||
@@ -7,12 +7,12 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionAdminService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
@@ -27,14 +27,10 @@ import {
|
||||
SearchModule,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
import { NewCipherMenuComponent } from "@bitwarden/vault";
|
||||
import { NewCipherMenuComponent, All, RoutedVaultFilterModel } from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { CollectionDialogTabType } from "../../shared/components/collection-dialog";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
|
||||
@@ -27,19 +27,22 @@ import {
|
||||
takeUntil,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
CollectionService,
|
||||
CollectionView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionAdminService, CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { NoResults } from "@bitwarden/assets/svg";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import {
|
||||
getFlatCollectionTree,
|
||||
getNestedCollectionTree,
|
||||
} from "@bitwarden/common/admin-console/utils";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
@@ -81,6 +84,13 @@ import {
|
||||
CollectionAssignmentResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
createFilterFunction,
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
VaultFilter,
|
||||
} from "@bitwarden/vault";
|
||||
import {
|
||||
OrganizationFreeTrialWarningComponent,
|
||||
@@ -102,15 +112,6 @@ import {
|
||||
BulkDeleteDialogResult,
|
||||
openBulkDeleteDialog,
|
||||
} from "../../../vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
|
||||
import { VaultFilterService } from "../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { RoutedVaultFilterBridgeService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { createFilterFunction } from "../../../vault/individual-vault/vault-filter/shared/models/filter-function";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "../../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { GroupApiService, GroupView } from "../core";
|
||||
import { openEntityEventsDialog } from "../manage/entity-events.component";
|
||||
@@ -126,7 +127,6 @@ import {
|
||||
BulkCollectionsDialogResult,
|
||||
} from "./bulk-collections-dialog";
|
||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||
import { getFlatCollectionTree, getNestedCollectionTree } from "./utils";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
export interface AddEditGroupDetail {
|
||||
id: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
|
||||
import { GroupDetailsResponse } from "../services/group/responses/group.response";
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
OrganizationUserDetailsResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
export class OrganizationUserAdminView {
|
||||
id: string;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
OrganizationUserUserDetailsResponse,
|
||||
CollectionAccessSelectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
|
||||
export class OrganizationUserView {
|
||||
id: string;
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -17,15 +17,15 @@ import {
|
||||
} from "rxjs";
|
||||
import { debounceTime, first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionData,
|
||||
Collection,
|
||||
CollectionView,
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
Collection,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
|
||||
@@ -15,11 +15,8 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
OrganizationUserApiService,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -30,6 +27,11 @@ import {
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
CollectionView,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, from, map, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
Collection,
|
||||
CollectionData,
|
||||
CollectionDetailsResponse,
|
||||
CollectionService,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { SelectItemView } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../core";
|
||||
|
||||
@@ -18,19 +18,21 @@ import {
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserUserMiniResponse,
|
||||
CollectionResponse,
|
||||
CollectionView,
|
||||
CollectionService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
CollectionView,
|
||||
CollectionResponse,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from "@angular/forms";
|
||||
import { combineLatest, map, Observable, of } from "rxjs";
|
||||
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Collection } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
|
||||
@@ -17,14 +17,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService, CipherFormConfigService } from "@bitwarden/vault";
|
||||
import {
|
||||
PasswordRepromptService,
|
||||
CipherFormConfigService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../exposed-passwords-report.component";
|
||||
|
||||
|
||||
@@ -12,14 +12,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
CipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent } from "../inactive-two-factor-report.component";
|
||||
|
||||
|
||||
@@ -16,14 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
CipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } from "../reused-passwords-report.component";
|
||||
|
||||
|
||||
@@ -16,14 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
CipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent } from "../unsecured-websites-report.component";
|
||||
|
||||
|
||||
@@ -17,14 +17,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
CipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from "../weak-passwords-report.component";
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault";
|
||||
import {
|
||||
CipherFormConfigService,
|
||||
DefaultCipherFormConfigService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
import { RoutedVaultFilterBridgeService } from "../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
|
||||
import { BreachReportComponent } from "./pages/breach-report.component";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionAdminService } from "@bitwarden/admin-console/common";
|
||||
import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogRef, DIALOG_DATA, DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultItemDialogComponent } from "./vault-item-dialog.component";
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable, Subject, switchMap } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
@@ -63,11 +63,11 @@ import {
|
||||
CipherViewComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
RoutedVaultFilterService,
|
||||
RoutedVaultFilterModel,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Unassigned,
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { CollectionPermission } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
export interface VaultItem<C extends CipherViewLike> {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { of, Subject } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -11,8 +11,7 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { MenuModule, TableModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { RoutedVaultFilterModel } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { RoutedVaultFilterService, RoutedVaultFilterModel } from "@bitwarden/vault";
|
||||
|
||||
import { VaultItem } from "./vault-item";
|
||||
import { VaultItemsComponent } from "./vault-items.component";
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
CollectionView,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
@@ -27,7 +31,7 @@ import {
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { SortDirection, TableDataSource } from "@bitwarden/components";
|
||||
import { OrganizationId } from "@bitwarden/sdk-internal";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/vault";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
} from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
@@ -41,7 +41,7 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { LayoutComponent, StorybookGlobalStateProvider } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/vault";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { Unassigned } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -32,12 +32,12 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { OrganizationFilter } from "@bitwarden/vault";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { EnrollMasterPasswordReset } from "../../../../admin-console/organizations/users/enroll-master-password-reset.component";
|
||||
import { LinkSsoService } from "../../../../auth/core/services";
|
||||
import { OptionsInput } from "../shared/components/vault-filter-section.component";
|
||||
import { OrganizationFilter } from "../shared/models/vault-filter.type";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
|
||||
@@ -27,22 +27,19 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
|
||||
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
|
||||
import {
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
VaultFilterList,
|
||||
VaultFilterSection,
|
||||
VaultFilterType,
|
||||
} from "../shared/models/vault-filter-section.type";
|
||||
import { VaultFilter } from "../shared/models/vault-filter.model";
|
||||
import {
|
||||
VaultFilter,
|
||||
CipherStatus,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "../shared/models/vault-filter.type";
|
||||
} from "@bitwarden/vault";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
|
||||
import { OrganizationOptionsComponent } from "./organization-options.component";
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} 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>>;
|
||||
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>>;
|
||||
abstract getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
|
||||
abstract setCollapsedFilterNodes: (
|
||||
collapsedFilterNodes: Set<string>,
|
||||
userId: UserId,
|
||||
) => Promise<void>;
|
||||
abstract expandOrgFilter: (userId: UserId) => Promise<void>;
|
||||
abstract getOrganizationFilter: () => Observable<Organization>;
|
||||
abstract setOrganizationFilter: (organization: Organization) => void;
|
||||
abstract buildTypeTree: (
|
||||
head: CipherTypeFilter,
|
||||
array: CipherTypeFilter[],
|
||||
) => Observable<TreeNode<CipherTypeFilter>>;
|
||||
// TODO: Remove this from org vault when collection admin service adopts state management
|
||||
abstract reloadCollections?: (collections: CollectionAdminView[]) => void;
|
||||
abstract clearOrganizationFilter: () => void;
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
|
||||
import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model";
|
||||
import { RoutedVaultFilterModel, All } from "../shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "../shared/models/vault-filter.model";
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "../shared/models/vault-filter.type";
|
||||
|
||||
import { VaultFilterService } from "./abstractions/vault-filter.service";
|
||||
import { RoutedVaultFilterService } from "./routed-vault-filter.service";
|
||||
|
||||
/**
|
||||
* This file is part of a layer that is used to temporary bridge between URL filtering and the old state-in-code method.
|
||||
* This should be removed after we have refactored the {@link VaultItemsComponent} and introduced vertical navigation
|
||||
* (which will refactor the {@link VaultFilterComponent}).
|
||||
*
|
||||
* This class listens to both the new {@link RoutedVaultFilterService} and the old {@link VaultFilterService}.
|
||||
* When a new filter is emitted the service uses the ids to find the corresponding tree nodes needed for
|
||||
* the old {@link VaultFilter} model. It then emits a bridge model that contains this information.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RoutedVaultFilterBridgeService {
|
||||
readonly activeFilter$: Observable<VaultFilter>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private routedVaultFilterService: RoutedVaultFilterService,
|
||||
legacyVaultFilterService: VaultFilterService,
|
||||
) {
|
||||
this.activeFilter$ = combineLatest([
|
||||
routedVaultFilterService.filter$,
|
||||
legacyVaultFilterService.collectionTree$,
|
||||
legacyVaultFilterService.folderTree$,
|
||||
legacyVaultFilterService.organizationTree$,
|
||||
legacyVaultFilterService.cipherTypeTree$,
|
||||
]).pipe(
|
||||
map(([filter, collectionTree, folderTree, organizationTree, cipherTypeTree]) => {
|
||||
const legacyFilter = isAdminConsole(filter)
|
||||
? createLegacyFilterForAdminConsole(filter, collectionTree, cipherTypeTree)
|
||||
: createLegacyFilterForEndUser(
|
||||
filter,
|
||||
collectionTree,
|
||||
folderTree,
|
||||
organizationTree,
|
||||
cipherTypeTree,
|
||||
);
|
||||
|
||||
return new RoutedVaultFilterBridge(filter, legacyFilter, this);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
navigate(filter: RoutedVaultFilterModel) {
|
||||
const [commands, extras] = this.routedVaultFilterService.createRoute(filter);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(commands, extras);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the filtering is being done as part of admin console.
|
||||
* Admin console can be identified by checking if the `organizationId`
|
||||
* is part of the path.
|
||||
*
|
||||
* @param filter Model to check if origin is admin console
|
||||
* @returns true if filtering being done as part of admin console
|
||||
*/
|
||||
function isAdminConsole(filter: RoutedVaultFilterModel) {
|
||||
return filter.organizationIdParamType === "path";
|
||||
}
|
||||
|
||||
function createLegacyFilterForAdminConsole(
|
||||
filter: RoutedVaultFilterModel,
|
||||
collectionTree: TreeNode<CollectionFilter>,
|
||||
cipherTypeTree: TreeNode<CipherTypeFilter>,
|
||||
): VaultFilter {
|
||||
const legacyFilter = new VaultFilter();
|
||||
|
||||
if (filter.collectionId === undefined && filter.type === undefined) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
"AllCollections",
|
||||
);
|
||||
} else if (filter.collectionId !== undefined && filter.collectionId === Unassigned) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(collectionTree, null);
|
||||
} else if (filter.collectionId !== undefined) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
filter.collectionId,
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.collectionId === undefined && filter.type === All) {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
"AllItems",
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type === "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(
|
||||
{ id: "trash", name: "", type: "trash", icon: "" },
|
||||
null,
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type !== "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
filter.type,
|
||||
);
|
||||
}
|
||||
|
||||
return legacyFilter;
|
||||
}
|
||||
|
||||
function createLegacyFilterForEndUser(
|
||||
filter: RoutedVaultFilterModel,
|
||||
collectionTree: TreeNode<CollectionFilter>,
|
||||
folderTree: TreeNode<FolderFilter>,
|
||||
organizationTree: TreeNode<OrganizationFilter>,
|
||||
cipherTypeTree: TreeNode<CipherTypeFilter>,
|
||||
): VaultFilter {
|
||||
const legacyFilter = new VaultFilter();
|
||||
|
||||
if (filter.collectionId !== undefined && filter.collectionId === Unassigned) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(collectionTree, null);
|
||||
} else if (filter.collectionId !== undefined && filter.collectionId === All) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
"AllCollections",
|
||||
);
|
||||
} else if (filter.collectionId !== undefined) {
|
||||
legacyFilter.selectedCollectionNode = ServiceUtils.getTreeNodeObject(
|
||||
collectionTree,
|
||||
filter.collectionId,
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.folderId !== undefined && filter.folderId === Unassigned) {
|
||||
legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, null);
|
||||
} else if (filter.folderId !== undefined && filter.folderId !== Unassigned) {
|
||||
legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, filter.folderId);
|
||||
}
|
||||
|
||||
if (filter.organizationId !== undefined && filter.organizationId === Unassigned) {
|
||||
legacyFilter.selectedOrganizationNode = ServiceUtils.getTreeNodeObject(
|
||||
organizationTree,
|
||||
"MyVault",
|
||||
);
|
||||
} else if (filter.organizationId !== undefined && filter.organizationId !== Unassigned) {
|
||||
legacyFilter.selectedOrganizationNode = ServiceUtils.getTreeNodeObject(
|
||||
organizationTree,
|
||||
filter.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.type === undefined) {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
"AllItems",
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type === "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(
|
||||
{ id: "trash", name: "", type: "trash", icon: "" },
|
||||
null,
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type === "archive") {
|
||||
legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(
|
||||
{ id: "archive", name: "", type: "archive", icon: "" },
|
||||
null,
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type !== "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
filter.type,
|
||||
);
|
||||
}
|
||||
|
||||
return legacyFilter;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { ActivatedRoute, NavigationExtras } from "@angular/router";
|
||||
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
isRoutedVaultFilterItemType,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../shared/models/routed-vault-filter.model";
|
||||
|
||||
/**
|
||||
* This service is an abstraction layer on top of ActivatedRoute that
|
||||
* encapsulates the logic of how filters are stored in the URL.
|
||||
*
|
||||
* The service builds and emits filter models based on URL params and
|
||||
* also contains a method for generating routes to corresponding to those params.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RoutedVaultFilterService implements OnDestroy {
|
||||
private onDestroy = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Filter values extracted from the URL.
|
||||
* To change the values use {@link RoutedVaultFilterService.createRoute}.
|
||||
*/
|
||||
filter$: Observable<RoutedVaultFilterModel>;
|
||||
|
||||
constructor(activatedRoute: ActivatedRoute) {
|
||||
this.filter$ = combineLatest([activatedRoute.paramMap, activatedRoute.queryParamMap]).pipe(
|
||||
map(([params, queryParams]) => {
|
||||
const unsafeType = queryParams.get("type");
|
||||
const type = isRoutedVaultFilterItemType(unsafeType) ? unsafeType : undefined;
|
||||
|
||||
return {
|
||||
collectionId: (queryParams.get("collectionId") as CollectionId) ?? undefined,
|
||||
folderId: queryParams.get("folderId") ?? undefined,
|
||||
organizationId:
|
||||
(params.get("organizationId") as OrganizationId) ??
|
||||
(queryParams.get("organizationId") as OrganizationId) ??
|
||||
undefined,
|
||||
organizationIdParamType:
|
||||
params.get("organizationId") != undefined ? ("path" as const) : ("query" as const),
|
||||
type,
|
||||
};
|
||||
}),
|
||||
takeUntil(this.onDestroy),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a route that can be used to modify filters with Router or RouterLink.
|
||||
* This method is specifically built to leave other query parameters untouched,
|
||||
* meaning that navigation will only affect filters and not e.g. `cipherId`.
|
||||
* To subscribe to changes use {@link RoutedVaultFilterService.filter$}.
|
||||
*
|
||||
* Note:
|
||||
* This method currently only supports changing filters that are stored
|
||||
* in query parameters. This means that {@link RoutedVaultFilterModel.organizationId}
|
||||
* will be ignored if {@link RoutedVaultFilterModel.organizationIdParamType}
|
||||
* is set to `path`.
|
||||
*
|
||||
* @param filter Filter values that should be applied to the URL.
|
||||
* @returns route that can be used with Router or RouterLink
|
||||
*/
|
||||
createRoute(filter: RoutedVaultFilterModel): [commands: any[], extras?: NavigationExtras] {
|
||||
const commands: string[] = [];
|
||||
const extras: NavigationExtras = {
|
||||
queryParams: {
|
||||
collectionId: filter.collectionId ?? null,
|
||||
folderId: filter.folderId ?? null,
|
||||
organizationId:
|
||||
filter.organizationIdParamType === "path" ? null : (filter.organizationId ?? null),
|
||||
type: filter.type ?? null,
|
||||
},
|
||||
queryParamsHandling: "merge",
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
};
|
||||
return [commands, extras];
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
import {
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/../spec/fake-account-service";
|
||||
import { FakeSingleUserState } from "@bitwarden/common/../spec/fake-state";
|
||||
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of, ReplaySubject } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionType,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import * as vaultFilterSvc from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
|
||||
jest.mock("@bitwarden/angular/vault/vault-filter/services/vault-filter.service", () => ({
|
||||
sortDefaultCollections: jest.fn(() => []),
|
||||
}));
|
||||
|
||||
describe("vault filter service", () => {
|
||||
let vaultFilterService: VaultFilterService;
|
||||
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let folderService: MockProxy<FolderService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let collectionService: MockProxy<CollectionService>;
|
||||
let organizations: ReplaySubject<Organization[]>;
|
||||
let folderViews: ReplaySubject<FolderView[]>;
|
||||
let collectionViews: ReplaySubject<CollectionView[]>;
|
||||
let cipherViews: ReplaySubject<CipherView[]>;
|
||||
let organizationDataOwnershipPolicy: ReplaySubject<boolean>;
|
||||
let singleOrgPolicy: ReplaySubject<boolean>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
let collapsedGroupingsState: FakeSingleUserState<string[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
organizationService = mock<OrganizationService>();
|
||||
folderService = mock<FolderService>();
|
||||
cipherService = mock<CipherService>();
|
||||
policyService = mock<PolicyService>();
|
||||
i18nService = mock<I18nService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
i18nService.collator = new Intl.Collator("en-US");
|
||||
collectionService = mock<CollectionService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
organizations = new ReplaySubject<Organization[]>(1);
|
||||
folderViews = new ReplaySubject<FolderView[]>(1);
|
||||
collectionViews = new ReplaySubject<CollectionView[]>(1);
|
||||
cipherViews = new ReplaySubject<CipherView[]>(1);
|
||||
organizationDataOwnershipPolicy = new ReplaySubject<boolean>(1);
|
||||
singleOrgPolicy = new ReplaySubject<boolean>(1);
|
||||
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
organizationService.memberOrganizations$.mockReturnValue(organizations);
|
||||
folderService.folderViews$.mockReturnValue(folderViews);
|
||||
collectionService.decryptedCollections$.mockReturnValue(collectionViews);
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.OrganizationDataOwnership, mockUserId)
|
||||
.mockReturnValue(organizationDataOwnershipPolicy);
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.SingleOrg, mockUserId)
|
||||
.mockReturnValue(singleOrgPolicy);
|
||||
cipherService.cipherListViews$.mockReturnValue(cipherViews);
|
||||
|
||||
vaultFilterService = new VaultFilterService(
|
||||
organizationService,
|
||||
folderService,
|
||||
cipherService,
|
||||
policyService,
|
||||
i18nService,
|
||||
stateProvider,
|
||||
collectionService,
|
||||
accountService,
|
||||
configService,
|
||||
);
|
||||
collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS);
|
||||
organizations.next([]);
|
||||
});
|
||||
|
||||
describe("collapsed filter nodes", () => {
|
||||
const nodes = new Set(["1", "2"]);
|
||||
|
||||
it("should update the collapsedFilterNodes$", async () => {
|
||||
await vaultFilterService.setCollapsedFilterNodes(nodes, mockUserId);
|
||||
|
||||
const collapsedGroupingsState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
COLLAPSED_GROUPINGS,
|
||||
);
|
||||
expect(await firstValueFrom(collapsedGroupingsState.state$)).toEqual(Array.from(nodes));
|
||||
expect(collapsedGroupingsState.nextMock).toHaveBeenCalledWith(Array.from(nodes));
|
||||
});
|
||||
|
||||
it("loads from state on initialization", async () => {
|
||||
collapsedGroupingsState.nextState(["1", "2"]);
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.collapsedFilterNodes$)).resolves.toEqual(
|
||||
nodes,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("organizations", () => {
|
||||
beforeEach(() => {
|
||||
const storedOrgs = [createOrganization("1", "org1"), createOrganization("2", "org2")];
|
||||
organizations.next(storedOrgs);
|
||||
organizationDataOwnershipPolicy.next(false);
|
||||
singleOrgPolicy.next(false);
|
||||
});
|
||||
|
||||
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 organization data ownership policy is enabled", async () => {
|
||||
organizationDataOwnershipPolicy.next(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 () => {
|
||||
singleOrgPolicy.next(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 organization data ownership policies are enabled", async () => {
|
||||
singleOrgPolicy.next(true);
|
||||
organizationDataOwnershipPolicy.next(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"),
|
||||
];
|
||||
cipherViews.next(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"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns current organization", () => {
|
||||
vaultFilterService.getOrganizationFilter().subscribe((org) => {
|
||||
expect(org.id).toEqual("org test id");
|
||||
expect(org.identifier).toEqual("Test Org");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
cipherViews.next([]);
|
||||
|
||||
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"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.filteredCollections$)).resolves.toEqual([
|
||||
createCollectionView("1", "collection 1", "org test id"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collection tree", () => {
|
||||
it("returns tree with children", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 3", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
|
||||
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-2", "id-3"]);
|
||||
});
|
||||
|
||||
it("returns tree where non-existing collections are excluded from children", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
|
||||
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-3"]);
|
||||
expect(result.children[0].children[0].node.name).toBe("Collection 2/Collection 3");
|
||||
});
|
||||
|
||||
it("returns tree with parents", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
createCollectionView("id-4", "Collection 1/Collection 4", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
const c1 = result.children[0];
|
||||
const c2 = c1.children[0];
|
||||
const c3 = c2.children[0];
|
||||
const c4 = c1.children[1];
|
||||
expect(c2.parent.node.id).toEqual("id-1");
|
||||
expect(c3.parent.node.id).toEqual("id-2");
|
||||
expect(c4.parent.node.id).toEqual("id-1");
|
||||
});
|
||||
|
||||
it("returns tree where non-existing collections are excluded from parents", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
const c1 = result.children[0];
|
||||
const c3 = c1.children[0];
|
||||
expect(c3.parent.node.id).toEqual("id-1");
|
||||
});
|
||||
|
||||
it("calls sortDefaultCollections with the correct args", async () => {
|
||||
const storedOrgs = [
|
||||
createOrganization("id-defaultOrg1", "org1"),
|
||||
createOrganization("id-defaultOrg2", "org2"),
|
||||
];
|
||||
organizations.next(storedOrgs);
|
||||
|
||||
const storedCollections = [
|
||||
createCollectionView("id-2", "Collection 2", "org test id"),
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView(
|
||||
"id-3",
|
||||
"Default User Collection - Org 2",
|
||||
"id-defaultOrg2",
|
||||
CollectionTypes.DefaultUserCollection,
|
||||
),
|
||||
createCollectionView(
|
||||
"id-4",
|
||||
"Default User Collection - Org 1",
|
||||
"id-defaultOrg1",
|
||||
CollectionTypes.DefaultUserCollection,
|
||||
),
|
||||
];
|
||||
collectionViews.next(storedCollections);
|
||||
collectionService.groupByOrganization.mockReturnValue(
|
||||
new Map([["org test id" as OrganizationId, storedCollections]]),
|
||||
);
|
||||
|
||||
await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(vaultFilterSvc.sortDefaultCollections).toHaveBeenCalledWith(
|
||||
storedCollections,
|
||||
storedOrgs,
|
||||
i18nService.collator,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createOrganization(id: string, name: string) {
|
||||
const org = new Organization();
|
||||
org.id = id;
|
||||
org.name = name;
|
||||
org.identifier = name;
|
||||
org.isMember = true;
|
||||
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,
|
||||
type?: CollectionType,
|
||||
): CollectionView {
|
||||
const collection = new CollectionView({
|
||||
id: id as CollectionId,
|
||||
name,
|
||||
organizationId: orgId as OrganizationId,
|
||||
});
|
||||
|
||||
if (type) {
|
||||
collection.type = type;
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
});
|
||||
@@ -1,364 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
import { cloneCollection } from "@bitwarden/web-vault/app/admin-console/organizations/collections";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "../shared/models/vault-filter.type";
|
||||
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "./abstractions/vault-filter.service";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
memberOrganizations$ = this.activeUserId$.pipe(
|
||||
switchMap((id) => this.organizationService.memberOrganizations$(id)),
|
||||
);
|
||||
|
||||
collapsedFilterNodes$ = this.activeUserId$.pipe(
|
||||
switchMap((id) => this.collapsedGroupingsState(id).state$),
|
||||
map((state) => new Set(state)),
|
||||
);
|
||||
|
||||
organizationTree$: Observable<TreeNode<OrganizationFilter>> = combineLatest([
|
||||
this.memberOrganizations$,
|
||||
this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)),
|
||||
),
|
||||
this.activeUserId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
||||
),
|
||||
),
|
||||
]).pipe(
|
||||
switchMap(([orgs, singleOrgPolicy, organizationDataOwnershipPolicy]) =>
|
||||
this.buildOrganizationTree(orgs, singleOrgPolicy, organizationDataOwnershipPolicy),
|
||||
),
|
||||
);
|
||||
|
||||
protected _organizationFilter = new BehaviorSubject<Organization>(null);
|
||||
|
||||
filteredFolders$: Observable<FolderView[]> = this.activeUserId$.pipe(
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.folderService.folderViews$(userId),
|
||||
this.cipherService.cipherListViews$(userId),
|
||||
this._organizationFilter,
|
||||
]),
|
||||
),
|
||||
filter(([folders, ciphers, org]) => !!ciphers), // ciphers may be null, meaning decryption is in progress. Ignore this emission
|
||||
switchMap(([folders, ciphers, org]) => {
|
||||
return this.filterFolders(folders, ciphers, org);
|
||||
}),
|
||||
);
|
||||
|
||||
folderTree$: Observable<TreeNode<FolderFilter>> = this.filteredFolders$.pipe(
|
||||
map((folders) => this.buildFolderTree(folders)),
|
||||
);
|
||||
|
||||
filteredCollections$: Observable<CollectionView[]> = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
),
|
||||
this._organizationFilter,
|
||||
]).pipe(switchMap(([collections, org]) => this.filterCollections(collections, org)));
|
||||
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>> = combineLatest([
|
||||
this.filteredCollections$,
|
||||
this.memberOrganizations$,
|
||||
]).pipe(
|
||||
map(([collections, organizations]) => this.buildCollectionTree(collections, organizations)),
|
||||
);
|
||||
|
||||
cipherTypeTree$: Observable<TreeNode<CipherTypeFilter>> = this.buildCipherTypeTree();
|
||||
|
||||
private collapsedGroupingsState(userId: UserId): SingleUserState<string[]> {
|
||||
return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected organizationService: OrganizationService,
|
||||
protected folderService: FolderService,
|
||||
protected cipherService: CipherService,
|
||||
protected policyService: PolicyService,
|
||||
protected i18nService: I18nService,
|
||||
protected stateProvider: StateProvider,
|
||||
protected collectionService: CollectionService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async getCollectionNodeFromTree(id: string) {
|
||||
const collections = await firstValueFrom(this.collectionTree$);
|
||||
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionFilter>;
|
||||
}
|
||||
|
||||
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>, userId: UserId): Promise<void> {
|
||||
await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes));
|
||||
}
|
||||
|
||||
protected async getCollapsedFilterNodes(): Promise<Set<string>> {
|
||||
return await firstValueFrom(this.collapsedFilterNodes$);
|
||||
}
|
||||
|
||||
getOrganizationFilter() {
|
||||
return this._organizationFilter;
|
||||
}
|
||||
|
||||
clearOrganizationFilter() {
|
||||
this._organizationFilter.next(null);
|
||||
}
|
||||
|
||||
setOrganizationFilter(organization: Organization) {
|
||||
if (organization?.id != "AllVaults") {
|
||||
this._organizationFilter.next(organization);
|
||||
} else {
|
||||
this._organizationFilter.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
async expandOrgFilter(userId: UserId) {
|
||||
const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$);
|
||||
if (!collapsedFilterNodes.has("AllVaults")) {
|
||||
return;
|
||||
}
|
||||
collapsedFilterNodes.delete("AllVaults");
|
||||
await this.setCollapsedFilterNodes(collapsedFilterNodes, userId);
|
||||
}
|
||||
|
||||
protected async buildOrganizationTree(
|
||||
orgs: Organization[],
|
||||
singleOrgPolicy: boolean,
|
||||
organizationDataOwnershipPolicy: boolean,
|
||||
): Promise<TreeNode<OrganizationFilter>> {
|
||||
const headNode = this.getOrganizationFilterHead();
|
||||
if (!organizationDataOwnershipPolicy) {
|
||||
const myVaultNode = this.getOrganizationFilterMyVault();
|
||||
headNode.children.push(myVaultNode);
|
||||
}
|
||||
if (singleOrgPolicy) {
|
||||
orgs = orgs.slice(0, 1);
|
||||
}
|
||||
if (orgs) {
|
||||
const orgNodes: TreeNode<OrganizationFilter>[] = [];
|
||||
orgs.forEach((org) => {
|
||||
const orgCopy = org as OrganizationFilter;
|
||||
orgCopy.icon = "bwi-business";
|
||||
const node = new TreeNode<OrganizationFilter>(orgCopy, headNode, orgCopy.name);
|
||||
orgNodes.push(node);
|
||||
});
|
||||
// Sort organization nodes, then add them to the list after 'My Vault' and 'All Vaults' if present
|
||||
orgNodes.sort((a, b) => a.node.name.localeCompare(b.node.name));
|
||||
headNode.children.push(...orgNodes);
|
||||
}
|
||||
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" as OrganizationId;
|
||||
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, headNode, 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[],
|
||||
orgs?: Organization[],
|
||||
): TreeNode<CollectionFilter> {
|
||||
const headNode = this.getCollectionFilterHead();
|
||||
if (!collections) {
|
||||
return headNode;
|
||||
}
|
||||
const all: TreeNode<CollectionFilter>[] = [];
|
||||
collections = sortDefaultCollections(collections, orgs, this.i18nService.collator);
|
||||
const groupedByOrg = this.collectionService.groupByOrganization(collections);
|
||||
|
||||
for (const group of groupedByOrg.values()) {
|
||||
const nodes: TreeNode<CollectionFilter>[] = [];
|
||||
for (const c of group) {
|
||||
const collectionCopy = cloneCollection(
|
||||
new CollectionView({ ...c, name: c.name }),
|
||||
) as CollectionFilter;
|
||||
collectionCopy.icon =
|
||||
c.type === CollectionTypes.DefaultUserCollection ? "bwi-user" : "bwi-collection-shared";
|
||||
const parts = c.name ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter);
|
||||
}
|
||||
all.push(...nodes);
|
||||
}
|
||||
|
||||
all.forEach((n) => {
|
||||
n.parent = headNode;
|
||||
headNode.children.push(n);
|
||||
});
|
||||
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getCollectionFilterHead(): TreeNode<CollectionFilter> {
|
||||
const head = CollectionView.vaultFilterHead() as CollectionFilter;
|
||||
return new TreeNode<CollectionFilter>(head, null, "collections", "AllCollections");
|
||||
}
|
||||
|
||||
protected async filterFolders(
|
||||
storedFolders: FolderView[],
|
||||
ciphers: CipherView[] | CipherListView[],
|
||||
org?: Organization,
|
||||
): Promise<FolderView[]> {
|
||||
// If no org or "My Vault" is selected, show all folders
|
||||
if (org?.id == null || org?.id == "MyVault") {
|
||||
return storedFolders;
|
||||
}
|
||||
|
||||
// Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder
|
||||
const orgCiphers = ciphers.filter((c) => c.organizationId == org?.id);
|
||||
return storedFolders.filter(
|
||||
(f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null,
|
||||
);
|
||||
}
|
||||
|
||||
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";
|
||||
folderCopy.fullName = f.name; // save full folder name before separating it into parts
|
||||
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;
|
||||
headNode.children.push(n);
|
||||
});
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getFolderFilterHead(): TreeNode<FolderFilter> {
|
||||
const head = new FolderView() as FolderFilter;
|
||||
return new TreeNode<FolderFilter>(head, null, "folders", "AllFolders");
|
||||
}
|
||||
|
||||
protected buildCipherTypeTree(): Observable<TreeNode<CipherTypeFilter>> {
|
||||
const allTypeFilters: CipherTypeFilter[] = [
|
||||
{
|
||||
id: "favorites",
|
||||
name: this.i18nService.t("favorites"),
|
||||
type: "favorites",
|
||||
icon: "bwi-star",
|
||||
},
|
||||
{
|
||||
id: "login",
|
||||
name: this.i18nService.t("typeLogin"),
|
||||
type: CipherType.Login,
|
||||
icon: "bwi-globe",
|
||||
},
|
||||
{
|
||||
id: "card",
|
||||
name: this.i18nService.t("typeCard"),
|
||||
type: CipherType.Card,
|
||||
icon: "bwi-credit-card",
|
||||
},
|
||||
{
|
||||
id: "identity",
|
||||
name: this.i18nService.t("typeIdentity"),
|
||||
type: CipherType.Identity,
|
||||
icon: "bwi-id-card",
|
||||
},
|
||||
{
|
||||
id: "note",
|
||||
name: this.i18nService.t("typeSecureNote"),
|
||||
type: CipherType.SecureNote,
|
||||
icon: "bwi-sticky-note",
|
||||
},
|
||||
{
|
||||
id: "sshKey",
|
||||
name: this.i18nService.t("typeSshKey"),
|
||||
type: CipherType.SshKey,
|
||||
icon: "bwi-key",
|
||||
},
|
||||
];
|
||||
|
||||
return this.buildTypeTree(
|
||||
{ id: "AllItems", name: "allItems", type: "all", icon: "" },
|
||||
allTypeFilters,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { VaultFilterService } from "../../services/abstractions/vault-filter.service";
|
||||
import { VaultFilterSection, VaultFilterType } from "../models/vault-filter-section.type";
|
||||
import { VaultFilter } from "../models/vault-filter.model";
|
||||
import {
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
VaultFilterSection,
|
||||
VaultFilterType,
|
||||
VaultFilter,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { createFilterFunction } from "./filter-function";
|
||||
import { All } from "./routed-vault-filter.model";
|
||||
|
||||
describe("createFilter", () => {
|
||||
describe("given a 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({ type: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ type: "trash" });
|
||||
|
||||
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({ type: "trash" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ type: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when type is not specified in filter", () => {
|
||||
const filterFunction = createFilterFunction({});
|
||||
|
||||
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({ type: "identity" });
|
||||
|
||||
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({ type: "favorites" });
|
||||
|
||||
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({ folderId: "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({ folderId: "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({ folderId: Unassigned });
|
||||
|
||||
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({
|
||||
collectionId: "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({
|
||||
collectionId: "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({
|
||||
organizationId: "nonMatchingOrganizationId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering for my vault only", () => {
|
||||
const filterFunction = createFilterFunction({ organizationId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering by All Collections", () => {
|
||||
const filterFunction = createFilterFunction({ collectionId: All });
|
||||
|
||||
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({ collectionId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when filter matches organization id", () => {
|
||||
const filterFunction = createFilterFunction({ organizationId: "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({ collectionId: Unassigned });
|
||||
|
||||
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({ organizationId: Unassigned });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
const cipher = new CipherView();
|
||||
|
||||
cipher.favorite = options.favorite ?? false;
|
||||
cipher.deletedDate = options.deletedDate;
|
||||
cipher.type = options.type ?? CipherType.Login;
|
||||
cipher.folderId = options.folderId;
|
||||
cipher.collectionIds = options.collectionIds;
|
||||
cipher.organizationId = options.organizationId;
|
||||
|
||||
return cipher;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
||||
|
||||
export type FilterFunction = (cipher: CipherViewLike) => boolean;
|
||||
|
||||
export function createFilterFunction(
|
||||
filter: RoutedVaultFilterModel,
|
||||
archiveEnabled?: boolean,
|
||||
): FilterFunction {
|
||||
return (cipher) => {
|
||||
const type = CipherViewLikeUtils.getType(cipher);
|
||||
const isDeleted = CipherViewLikeUtils.isDeleted(cipher);
|
||||
|
||||
if (filter.type === "favorites" && !cipher.favorite) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "card" && type !== CipherType.Card) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "identity" && type !== CipherType.Identity) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "login" && type !== CipherType.Login) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "note" && type !== CipherType.SecureNote) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "sshKey" && type !== CipherType.SshKey) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "trash" && !isDeleted) {
|
||||
return false;
|
||||
}
|
||||
// Hide trash unless explicitly selected
|
||||
if (filter.type !== "trash" && isDeleted) {
|
||||
return false;
|
||||
}
|
||||
// Archive filter logic is only applied if the feature flag is enabled
|
||||
if (archiveEnabled) {
|
||||
if (filter.type === "archive" && !CipherViewLikeUtils.isArchived(cipher)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter.type !== "archive" &&
|
||||
filter.type !== "trash" &&
|
||||
CipherViewLikeUtils.isArchived(cipher)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// No folder
|
||||
if (filter.folderId === Unassigned && cipher.folderId != null) {
|
||||
return false;
|
||||
}
|
||||
// Folder
|
||||
if (
|
||||
filter.folderId !== undefined &&
|
||||
filter.folderId !== All &&
|
||||
filter.folderId !== Unassigned &&
|
||||
cipher.folderId !== filter.folderId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// All collections (top level)
|
||||
if (filter.collectionId === All) {
|
||||
return false;
|
||||
}
|
||||
// Unassigned
|
||||
if (
|
||||
filter.collectionId === Unassigned &&
|
||||
(cipher.organizationId == null ||
|
||||
(cipher.collectionIds != null && cipher.collectionIds.length > 0))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// Collection
|
||||
if (
|
||||
filter.collectionId !== undefined &&
|
||||
filter.collectionId !== All &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
(cipher.collectionIds == null || !cipher.collectionIds.includes(filter.collectionId as any))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// My Vault
|
||||
if (filter.organizationId === Unassigned && cipher.organizationId != null) {
|
||||
return false;
|
||||
}
|
||||
// Organization
|
||||
else if (
|
||||
filter.organizationId !== undefined &&
|
||||
filter.organizationId !== Unassigned &&
|
||||
cipher.organizationId !== filter.organizationId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { RoutedVaultFilterBridgeService } from "../../services/routed-vault-filter-bridge.service";
|
||||
|
||||
import {
|
||||
All,
|
||||
isRoutedVaultFilterItemType,
|
||||
RoutedVaultFilterItemType,
|
||||
RoutedVaultFilterModel,
|
||||
} from "./routed-vault-filter.model";
|
||||
import { VaultFilter, VaultFilterFunction } from "./vault-filter.model";
|
||||
import {
|
||||
OrganizationFilter,
|
||||
CipherTypeFilter,
|
||||
FolderFilter,
|
||||
CollectionFilter,
|
||||
CipherStatus,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
/**
|
||||
* This file is part of a layer that is used to temporary bridge between URL filtering and the old state-in-code method.
|
||||
* This should be removed after we have refactored the {@link VaultItemsComponent} and introduced vertical navigation
|
||||
* (which will refactor the {@link VaultFilterComponent}).
|
||||
*
|
||||
* This model supplies legacy code with the old state-in-code models saved as tree nodes.
|
||||
* It can also receive requests to select a new tree node by using setters.
|
||||
* However instead of just replacing the tree node models, it requests a URL navigation,
|
||||
* thereby bridging between legacy and URL filtering.
|
||||
*/
|
||||
export class RoutedVaultFilterBridge implements VaultFilter {
|
||||
constructor(
|
||||
private routedFilter: RoutedVaultFilterModel,
|
||||
private legacyFilter: VaultFilter,
|
||||
private bridgeService: RoutedVaultFilterBridgeService,
|
||||
) {}
|
||||
get collectionBreadcrumbs(): TreeNode<CollectionFilter>[] {
|
||||
return this.legacyFilter.collectionBreadcrumbs;
|
||||
}
|
||||
get isCollectionSelected(): boolean {
|
||||
return this.legacyFilter.isCollectionSelected;
|
||||
}
|
||||
get isUnassignedCollectionSelected(): boolean {
|
||||
return this.legacyFilter.isUnassignedCollectionSelected;
|
||||
}
|
||||
get isMyVaultSelected(): boolean {
|
||||
return this.legacyFilter.isMyVaultSelected;
|
||||
}
|
||||
get selectedOrganizationNode(): TreeNode<OrganizationFilter> {
|
||||
return this.legacyFilter.selectedOrganizationNode;
|
||||
}
|
||||
set selectedOrganizationNode(value: TreeNode<OrganizationFilter>) {
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
organizationId: value?.node.id === "MyVault" ? Unassigned : value?.node.id,
|
||||
folderId: undefined,
|
||||
collectionId: undefined,
|
||||
});
|
||||
}
|
||||
get selectedCipherTypeNode(): TreeNode<CipherTypeFilter> {
|
||||
return this.legacyFilter.selectedCipherTypeNode;
|
||||
}
|
||||
set selectedCipherTypeNode(value: TreeNode<CipherTypeFilter>) {
|
||||
let type: RoutedVaultFilterItemType | undefined;
|
||||
|
||||
if (value?.node.id === "AllItems" && this.routedFilter.organizationIdParamType === "path") {
|
||||
type = All;
|
||||
} else if (
|
||||
value?.node.id === "AllItems" &&
|
||||
this.routedFilter.organizationIdParamType === "query"
|
||||
) {
|
||||
type = undefined;
|
||||
} else if (isRoutedVaultFilterItemType(value?.node.id)) {
|
||||
type = value?.node.id;
|
||||
}
|
||||
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
type,
|
||||
folderId: undefined,
|
||||
collectionId: undefined,
|
||||
});
|
||||
}
|
||||
get selectedFolderNode(): TreeNode<FolderFilter> {
|
||||
return this.legacyFilter.selectedFolderNode;
|
||||
}
|
||||
set selectedFolderNode(value: TreeNode<FolderFilter>) {
|
||||
const folderId = value != null && value.node.id === null ? Unassigned : value?.node.id;
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
folderId,
|
||||
type: undefined,
|
||||
collectionId: undefined,
|
||||
});
|
||||
}
|
||||
get selectedCollectionNode(): TreeNode<CollectionFilter> {
|
||||
return this.legacyFilter.selectedCollectionNode;
|
||||
}
|
||||
set selectedCollectionNode(value: TreeNode<CollectionFilter>) {
|
||||
let collectionId: CollectionId | All | Unassigned | undefined;
|
||||
|
||||
if (value != null && value.node.id === null) {
|
||||
collectionId = Unassigned;
|
||||
} else if (
|
||||
value?.node.id === "AllCollections" &&
|
||||
this.routedFilter.organizationIdParamType === "path"
|
||||
) {
|
||||
collectionId = undefined;
|
||||
} else if (
|
||||
value?.node.id === "AllCollections" &&
|
||||
this.routedFilter.organizationIdParamType === "query"
|
||||
) {
|
||||
collectionId = All;
|
||||
} else {
|
||||
collectionId = value?.node.id;
|
||||
}
|
||||
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
collectionId,
|
||||
type: undefined,
|
||||
folderId: undefined,
|
||||
});
|
||||
}
|
||||
get isFavorites(): boolean {
|
||||
return this.legacyFilter.isFavorites;
|
||||
}
|
||||
get isDeleted(): boolean {
|
||||
return this.legacyFilter.isDeleted;
|
||||
}
|
||||
get isArchived(): boolean {
|
||||
return this.legacyFilter.isArchived;
|
||||
}
|
||||
get organizationId(): string {
|
||||
return this.legacyFilter.organizationId;
|
||||
}
|
||||
get cipherType(): CipherType {
|
||||
return this.legacyFilter.cipherType;
|
||||
}
|
||||
get cipherStatus(): CipherStatus {
|
||||
return this.legacyFilter.cipherStatus;
|
||||
}
|
||||
get cipherTypeId(): string {
|
||||
return this.legacyFilter.cipherTypeId;
|
||||
}
|
||||
get folderId(): string {
|
||||
return this.legacyFilter.folderId;
|
||||
}
|
||||
get collectionId(): string {
|
||||
return this.legacyFilter.collectionId;
|
||||
}
|
||||
resetFilter(): void {
|
||||
this.bridgeService.navigate({
|
||||
...this.routedFilter,
|
||||
collectionId: undefined,
|
||||
folderId: undefined,
|
||||
organizationId:
|
||||
this.routedFilter.organizationIdParamType === "path"
|
||||
? this.routedFilter.organizationId
|
||||
: undefined,
|
||||
type: undefined,
|
||||
});
|
||||
}
|
||||
resetOrganization(): void {
|
||||
this.bridgeService.navigate({ ...this.routedFilter, organizationId: undefined });
|
||||
}
|
||||
buildFilter(): VaultFilterFunction {
|
||||
return this.legacyFilter.buildFilter();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
/**
|
||||
* A constant used to represent viewing "all" of a particular filter.
|
||||
*/
|
||||
export const All = "all";
|
||||
export type All = typeof All;
|
||||
|
||||
// TODO: Remove `All` when moving to vertical navigation.
|
||||
const itemTypes = [
|
||||
"favorites",
|
||||
"login",
|
||||
"card",
|
||||
"identity",
|
||||
"note",
|
||||
"sshKey",
|
||||
"archive",
|
||||
"trash",
|
||||
All,
|
||||
] as const;
|
||||
|
||||
export type RoutedVaultFilterItemType = (typeof itemTypes)[number];
|
||||
|
||||
export function isRoutedVaultFilterItemType(value: unknown): value is RoutedVaultFilterItemType {
|
||||
return itemTypes.includes(value as any);
|
||||
}
|
||||
|
||||
export interface RoutedVaultFilterModel {
|
||||
collectionId?: CollectionId | All | Unassigned;
|
||||
folderId?: string;
|
||||
organizationId?: OrganizationId | Unassigned;
|
||||
type?: RoutedVaultFilterItemType;
|
||||
|
||||
organizationIdParamType?: "path" | "query";
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
export type VaultFilterType =
|
||||
| OrganizationFilter
|
||||
| CipherTypeFilter
|
||||
| FolderFilter
|
||||
| CollectionFilter;
|
||||
|
||||
export const VaultFilterLabel = {
|
||||
OrganizationFilter: "organizationFilter",
|
||||
TypeFilter: "typeFilter",
|
||||
FolderFilter: "folderFilter",
|
||||
CollectionFilter: "collectionFilter",
|
||||
ArchiveFilter: "archiveFilter",
|
||||
TrashFilter: "trashFilter",
|
||||
} as const;
|
||||
|
||||
type VaultFilterLabel = UnionOfValues<typeof VaultFilterLabel>;
|
||||
|
||||
export type VaultFilterSection = {
|
||||
data$: Observable<TreeNode<VaultFilterType>>;
|
||||
header: {
|
||||
showHeader: boolean;
|
||||
isSelectable: boolean;
|
||||
};
|
||||
action: (filterNode: TreeNode<VaultFilterType>) => Promise<void>;
|
||||
edit?: {
|
||||
filterName: string;
|
||||
action: (filter: VaultFilterType) => void;
|
||||
};
|
||||
add?: {
|
||||
text: string;
|
||||
route?: string;
|
||||
action?: () => void;
|
||||
};
|
||||
options?: {
|
||||
component: any;
|
||||
};
|
||||
divider?: boolean;
|
||||
premiumOptions?: {
|
||||
/** When true, the premium badge will show on the filter for non-premium users. */
|
||||
showBadgeForNonPremium?: true;
|
||||
/**
|
||||
* Action to be called instead of applying the filter.
|
||||
* Useful when the user does not have access to a filter (e.g., premium feature)
|
||||
* and custom behavior is needed when invoking the filter.
|
||||
*/
|
||||
blockFilterAction?: () => Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
export type VaultFilterList = {
|
||||
[key in VaultFilterLabel]?: VaultFilterSection;
|
||||
};
|
||||
@@ -1,334 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
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" as CollectionId,
|
||||
}),
|
||||
});
|
||||
|
||||
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({
|
||||
name: options.name ?? "Test Name",
|
||||
id: options.id ?? null,
|
||||
organizationId: options.organizationId ?? "Org Id",
|
||||
}) as CollectionFilter;
|
||||
return new TreeNode<CollectionFilter>(collection, {} as TreeNode<CollectionFilter>);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CipherType, isCipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
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>;
|
||||
|
||||
/**
|
||||
* A list of collection filters that form a chain from the organization root to currently selected collection.
|
||||
* To be used when rendering a breadcrumb UI for navigating the collection hierarchy.
|
||||
* Begins from the organization root and excludes the currently selected collection.
|
||||
*/
|
||||
get collectionBreadcrumbs(): TreeNode<CollectionFilter>[] {
|
||||
if (!this.isCollectionSelected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const collections = [this.selectedCollectionNode];
|
||||
while (collections[collections.length - 1].parent != undefined) {
|
||||
collections.push(collections[collections.length - 1].parent);
|
||||
}
|
||||
|
||||
return collections.slice(1).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* The vault is filtered by a specific collection
|
||||
*/
|
||||
get isCollectionSelected(): boolean {
|
||||
return (
|
||||
this.selectedCollectionNode != null &&
|
||||
this.selectedCollectionNode.node.id !== "AllCollections"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The vault is filtered by the "Unassigned" collection
|
||||
*/
|
||||
get isUnassignedCollectionSelected(): boolean {
|
||||
return this.selectedCollectionNode != null && this.selectedCollectionNode.node.id === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The vault is filtered by the users individual vault
|
||||
*/
|
||||
get isMyVaultSelected(): boolean {
|
||||
return this.selectedOrganizationNode?.node.id === "MyVault";
|
||||
}
|
||||
|
||||
get isFavorites(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "favorites";
|
||||
}
|
||||
|
||||
get isDeleted(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "trash" ? true : null;
|
||||
}
|
||||
|
||||
get isArchived(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "archive";
|
||||
}
|
||||
|
||||
get organizationId(): string {
|
||||
return this.selectedOrganizationNode?.node.id;
|
||||
}
|
||||
|
||||
get cipherType(): CipherType {
|
||||
return isCipherType(this.selectedCipherTypeNode?.node.type)
|
||||
? this.selectedCipherTypeNode?.node.type
|
||||
: 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.isArchived && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isArchived;
|
||||
}
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
export type CipherStatus = "all" | "favorites" | "archive" | "trash" | CipherType;
|
||||
|
||||
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
|
||||
export type CollectionFilter = CollectionAdminView & {
|
||||
icon: string;
|
||||
};
|
||||
export type FolderFilter = FolderView & {
|
||||
icon: string;
|
||||
/**
|
||||
* Full folder name.
|
||||
*
|
||||
* Used for when the folder `name` property is be separated into parts.
|
||||
*/
|
||||
fullName?: string;
|
||||
};
|
||||
export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean };
|
||||
@@ -1,14 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SearchModule } from "@bitwarden/components";
|
||||
import { VaultFilterServiceAbstraction, VaultFilterService } from "@bitwarden/vault";
|
||||
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
|
||||
|
||||
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
|
||||
|
||||
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";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultFilterSharedModule, SearchModule, OrganizationWarningsModule],
|
||||
|
||||
@@ -3,13 +3,13 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionAdminService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
Unassigned,
|
||||
CollectionView,
|
||||
CollectionAdminService,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
@@ -24,16 +24,12 @@ import {
|
||||
MenuModule,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
import { NewCipherMenuComponent } from "@bitwarden/vault";
|
||||
import { NewCipherMenuComponent, All, RoutedVaultFilterModel } from "@bitwarden/vault";
|
||||
|
||||
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { PipesModule } from "../pipes/pipes.module";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-header",
|
||||
|
||||
@@ -25,13 +25,7 @@ import {
|
||||
tap,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
CollectionData,
|
||||
CollectionDetailsResponse,
|
||||
CollectionService,
|
||||
CollectionView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import {
|
||||
NoResults,
|
||||
@@ -50,7 +44,17 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
CollectionView,
|
||||
Unassigned,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import {
|
||||
getNestedCollectionTree,
|
||||
getFlatCollectionTree,
|
||||
} from "@bitwarden/common/admin-console/utils";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
@@ -97,6 +101,15 @@ import {
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultCipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
createFilterFunction,
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
VaultFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
VaultItemsTransferService,
|
||||
DefaultVaultItemsTransferService,
|
||||
} from "@bitwarden/vault";
|
||||
@@ -104,10 +117,6 @@ import { UnifiedUpgradePromptService } from "@bitwarden/web-vault/app/billing/in
|
||||
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
|
||||
import {
|
||||
getNestedCollectionTree,
|
||||
getFlatCollectionTree,
|
||||
} from "../../admin-console/organizations/collections";
|
||||
import {
|
||||
AutoConfirmPolicy,
|
||||
AutoConfirmPolicyDialogComponent,
|
||||
@@ -140,16 +149,6 @@ import {
|
||||
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
|
||||
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||
import { RoutedVaultFilterBridgeService } from "./vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "./vault-filter/services/routed-vault-filter.service";
|
||||
import { createFilterFunction } from "./vault-filter/shared/models/filter-function";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "./vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionAdminService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/vault";
|
||||
|
||||
import { AdminConsoleCipherFormConfigService } from "./admin-console-cipher-form-config.service";
|
||||
|
||||
|
||||
@@ -16,9 +16,12 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherFormConfig, CipherFormConfigService, CipherFormMode } from "@bitwarden/vault";
|
||||
|
||||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CipherFormConfigService,
|
||||
CipherFormMode,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
/** Admin Console implementation of the `CipherFormConfigService`. */
|
||||
@Injectable()
|
||||
|
||||
Reference in New Issue
Block a user