1
0
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:
Leslie Xiong
2026-01-15 10:17:00 -05:00
committed by GitHub
parent 6ef5241c29
commit 44bdaf71b3
142 changed files with 1217 additions and 507 deletions

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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()" />

View File

@@ -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();
});
});

View File

@@ -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",

View File

@@ -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,

View File

@@ -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)"
/>
}

View File

@@ -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();
}
}
}

View File

@@ -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>
}

View File

@@ -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);
}
}

View File

@@ -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>
}

View File

@@ -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;
}
}
}

View File

@@ -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"
/>

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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 }),
);
}

View File

@@ -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>
}

View File

@@ -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,
},
},
});
}
}

View File

@@ -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;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -1,2 +1 @@
export * from "./utils";
export * from "./collection-badge";

View File

@@ -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",

View File

@@ -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);
});
});
});

View File

@@ -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];
});
}

View File

@@ -1 +0,0 @@
export * from "./collection-utils";

View File

@@ -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

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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

View File

@@ -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";

View File

@@ -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;

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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> {

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}
});

View File

@@ -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,
);
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
};
}

View File

@@ -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();
}
}

View File

@@ -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";
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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;
};
}
}

View File

@@ -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 };

View File

@@ -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],

View File

@@ -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",

View File

@@ -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";

View File

@@ -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";

View File

@@ -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()