mirror of
https://github.com/bitwarden/browser
synced 2026-02-24 08:33:29 +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:
committed by
jaasen-livefront
parent
7088447046
commit
acad754540
@@ -2,7 +2,7 @@
|
||||
<app-side-nav slot="side-nav">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n" />
|
||||
|
||||
<bit-nav-item icon="bwi-vault" [text]="'vault' | i18n" route="new-vault" />
|
||||
<app-vault-filter />
|
||||
<app-send-filters-nav />
|
||||
|
||||
<bit-nav-item icon="bwi-generate" [text]="'generator' | i18n" (click)="openGenerator()" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { DialogService, NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component";
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
import { DesktopLayoutComponent } from "./desktop-layout.component";
|
||||
@@ -20,6 +21,13 @@ import { DesktopLayoutComponent } from "./desktop-layout.component";
|
||||
})
|
||||
class MockSendFiltersNavComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
template: "",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockVaultFiltersNavComponent {}
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
@@ -59,8 +67,8 @@ describe("DesktopLayoutComponent", () => {
|
||||
],
|
||||
})
|
||||
.overrideComponent(DesktopLayoutComponent, {
|
||||
remove: { imports: [SendFiltersNavComponent] },
|
||||
add: { imports: [MockSendFiltersNavComponent] },
|
||||
remove: { imports: [SendFiltersNavComponent, VaultFilterComponent] },
|
||||
add: { imports: [MockSendFiltersNavComponent, MockVaultFiltersNavComponent] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
@@ -93,4 +101,11 @@ describe("DesktopLayoutComponent", () => {
|
||||
|
||||
expect(sendFiltersNav).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders vault filters navigation component", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const vaultFiltersNav = compiled.querySelector("app-vault-filter");
|
||||
|
||||
expect(vaultFiltersNav).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PasswordManagerLogo } from "@bitwarden/assets/svg";
|
||||
import { DialogService, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component";
|
||||
import { ExportDesktopComponent } from "../tools/export/export-desktop.component";
|
||||
import { CredentialGeneratorComponent } from "../tools/generator/credential-generator.component";
|
||||
import { ImportDesktopComponent } from "../tools/import/import-desktop.component";
|
||||
@@ -22,6 +23,7 @@ import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
DesktopSideNavComponent,
|
||||
VaultFilterComponent,
|
||||
SendFiltersNavComponent,
|
||||
],
|
||||
templateUrl: "./desktop-layout.component.html",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { APP_INITIALIZER, NgModule } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, merge } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import {
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
PolicyService as PolicyServiceAbstraction,
|
||||
InternalPolicyService,
|
||||
@@ -107,6 +108,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic
|
||||
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
@@ -122,7 +124,15 @@ import {
|
||||
SessionTimeoutSettingsComponentService,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
DefaultSshImportPromptService,
|
||||
SshImportPromptService,
|
||||
VaultFilterServiceAbstraction,
|
||||
VaultFilterService,
|
||||
RoutedVaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
VAULT_FILTER_BASE_ROUTE,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopAuthRequestAnsweringService } from "../../auth/services/auth-request-answering/desktop-auth-request-answering.service";
|
||||
@@ -508,6 +518,34 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: SessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction, SessionTimeoutTypeService, PolicyServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
deps: [
|
||||
OrganizationService,
|
||||
FolderService,
|
||||
CipherServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
StateProvider,
|
||||
CollectionService,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VAULT_FILTER_BASE_ROUTE,
|
||||
useValue: "/new-vault",
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RoutedVaultFilterService,
|
||||
useClass: RoutedVaultFilterService,
|
||||
deps: [ActivatedRoute],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RoutedVaultFilterBridgeService,
|
||||
useClass: RoutedVaultFilterBridgeService,
|
||||
deps: [Router, RoutedVaultFilterService, VaultFilterServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestAnsweringService,
|
||||
useClass: DesktopAuthRequestAnsweringService,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
@if (collection().children.length) {
|
||||
<bit-nav-group
|
||||
[icon]="collection().node.icon"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (childCollection of collection().children; track childCollection.node.id) {
|
||||
<app-collection-filter [collection]="childCollection" [activeFilter]="activeFilter()" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
} @else {
|
||||
<bit-nav-item
|
||||
[icon]="collection().node.icon"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
/>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, input, computed } from "@angular/core";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { VaultFilter, CollectionFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-collection-filter",
|
||||
templateUrl: "collection-filter.component.html",
|
||||
imports: [A11yTitleDirective, NavigationModule],
|
||||
})
|
||||
export class CollectionFilterComponent {
|
||||
protected readonly collection = input<TreeNode<CollectionFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
|
||||
protected readonly displayName = computed<string>(() => {
|
||||
return this.collection().node.name;
|
||||
});
|
||||
|
||||
protected readonly isActive = computed<boolean>(() => {
|
||||
return (
|
||||
this.collection().node.id === this.activeFilter()?.collectionId &&
|
||||
!!this.activeFilter()?.selectedCollectionNode
|
||||
);
|
||||
});
|
||||
|
||||
protected applyFilter(event: Event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedCollectionNode = this.collection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
@if (folder().children.length) {
|
||||
<bit-nav-group
|
||||
[icon]="folder().node.icon"
|
||||
[class.active]="isActive()"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@if (folder()?.node.id) {
|
||||
<button
|
||||
type="button"
|
||||
slot="end"
|
||||
class="edit-button"
|
||||
bitIconButton="bwi-pencil"
|
||||
buttonType="nav-contrast"
|
||||
size="small"
|
||||
[label]="'editFolder' | i18n"
|
||||
(click)="editFolder(folder().node)"
|
||||
></button>
|
||||
}
|
||||
@for (childFolder of folder().children; track childFolder.node.id) {
|
||||
<app-folder-filter [folder]="childFolder" [activeFilter]="activeFilter()" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
} @else {
|
||||
<bit-nav-item
|
||||
[icon]="folder().node.icon"
|
||||
[forceActiveStyles]="isActive()"
|
||||
[text]="displayName()"
|
||||
variant="tree"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
>
|
||||
@if (folder()?.node.id) {
|
||||
<button
|
||||
type="button"
|
||||
slot="end"
|
||||
class="edit-button"
|
||||
bitIconButton="bwi-pencil"
|
||||
buttonType="nav-contrast"
|
||||
size="small"
|
||||
[label]="'editFolder' | i18n"
|
||||
(click)="editFolder(folder().node)"
|
||||
></button>
|
||||
}
|
||||
</bit-nav-item>
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component, input, computed, output } from "@angular/core";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { IconButtonModule, NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultFilter, FolderFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-folder-filter",
|
||||
templateUrl: "folder-filter.component.html",
|
||||
imports: [A11yTitleDirective, NavigationModule, IconButtonModule, I18nPipe],
|
||||
})
|
||||
export class FolderFilterComponent {
|
||||
protected readonly folder = input<TreeNode<FolderFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
protected onEditFolder = output<FolderFilter>();
|
||||
|
||||
protected readonly displayName = computed<string>(() => {
|
||||
return this.folder().node.name;
|
||||
});
|
||||
|
||||
protected readonly isActive = computed<boolean>(() => {
|
||||
return (
|
||||
this.folder().node.id === this.activeFilter()?.folderId &&
|
||||
!!this.activeFilter()?.selectedFolderNode
|
||||
);
|
||||
});
|
||||
|
||||
protected applyFilter(event: Event) {
|
||||
event.stopPropagation();
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedFolderNode = this.folder();
|
||||
}
|
||||
}
|
||||
|
||||
protected editFolder(folder: FolderFilter) {
|
||||
this.onEditFolder.emit(folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@if (show()) {
|
||||
<bit-nav-group
|
||||
icon="bwi-filter"
|
||||
(click)="applyAllVaultsFilter()"
|
||||
[text]="'allVaults' | i18n"
|
||||
[attr.aria-pressed]="!activeFilter()?.selectedOrganizationNode"
|
||||
[appA11yTitle]="'allVaults' | i18n"
|
||||
variant="tree"
|
||||
[open]="true"
|
||||
[forceActiveStyles]="!activeFilter()?.selectedOrganizationNode"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (organization of organizations().children ?? []; track organization.node.id) {
|
||||
<bit-nav-item
|
||||
[icon]="organization.node.icon"
|
||||
[forceActiveStyles]="organization.node.id === activeFilter()?.organizationId"
|
||||
[text]="organization.node.name"
|
||||
[appA11yTitle]="organization.node.name"
|
||||
(click)="applyFilter($event, organization)"
|
||||
/>
|
||||
@if (!organization.node.enabled) {
|
||||
<span class="tw-ml-auto">
|
||||
<i
|
||||
class="bwi bwi-fw bwi-exclamation-triangle text-danger mr-auto"
|
||||
[attr.aria-label]="'organizationIsDisabled' | i18n"
|
||||
[appA11yTitle]="'organizationIsDisabled' | i18n"
|
||||
></i>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</bit-nav-group>
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component, computed, input, inject } from "@angular/core";
|
||||
|
||||
import { DisplayMode } from "@bitwarden/angular/vault/vault-filter/models/display-mode";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ToastService, NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { OrganizationFilter, VaultFilter, VaultFilterServiceAbstraction } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-organization-filter",
|
||||
templateUrl: "organization-filter.component.html",
|
||||
imports: [A11yTitleDirective, NavigationModule, I18nPipe],
|
||||
})
|
||||
export class OrganizationFilterComponent {
|
||||
private toastService: ToastService = inject(ToastService);
|
||||
private i18nService: I18nService = inject(I18nService);
|
||||
private vaultFilterService: VaultFilterServiceAbstraction = inject(VaultFilterServiceAbstraction);
|
||||
|
||||
protected readonly hide = input(false);
|
||||
protected readonly organizations = input<TreeNode<OrganizationFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
protected readonly activeOrganizationDataOwnership = input<boolean>(false);
|
||||
protected readonly activeSingleOrganizationPolicy = input<boolean>(false);
|
||||
|
||||
protected readonly show = computed(() => {
|
||||
const hiddenDisplayModes: DisplayMode[] = [
|
||||
"singleOrganizationAndOrganizatonDataOwnershipPolicies",
|
||||
];
|
||||
return (
|
||||
!this.hide() &&
|
||||
this.organizations()?.children.length > 0 &&
|
||||
hiddenDisplayModes.indexOf(this.displayMode()) === -1
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly displayMode = computed<DisplayMode>(() => {
|
||||
let displayMode: DisplayMode = "organizationMember";
|
||||
if (this.organizations() == null || this.organizations().children.length < 1) {
|
||||
displayMode = "noOrganizations";
|
||||
} else if (this.activeOrganizationDataOwnership() && !this.activeSingleOrganizationPolicy()) {
|
||||
displayMode = "organizationDataOwnershipPolicy";
|
||||
} else if (!this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) {
|
||||
displayMode = "singleOrganizationPolicy";
|
||||
} else if (this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) {
|
||||
displayMode = "singleOrganizationAndOrganizatonDataOwnershipPolicies";
|
||||
}
|
||||
|
||||
return displayMode;
|
||||
});
|
||||
|
||||
protected applyFilter(event: Event, organization: TreeNode<OrganizationFilter>) {
|
||||
event.stopPropagation();
|
||||
if (!organization.node.enabled) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("disabledOrganizationFilterError"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.vaultFilterService.setOrganizationFilter(organization.node);
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedOrganizationNode = organization;
|
||||
}
|
||||
}
|
||||
|
||||
protected applyAllVaultsFilter() {
|
||||
this.vaultFilterService.clearOrganizationFilter();
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedOrganizationNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@if (!hideArchive()) {
|
||||
<bit-nav-item
|
||||
[icon]="archiveFilter.icon"
|
||||
[forceActiveStyles]="activeFilter()?.isArchived"
|
||||
(click)="handleArchiveFilter($event)"
|
||||
[text]="archiveFilter.name | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.isArchived"
|
||||
[appA11yTitle]="archiveFilter.name | i18n"
|
||||
/>
|
||||
@if (!(canArchive$ | async)) {
|
||||
<app-premium-badge />
|
||||
}
|
||||
}
|
||||
<bit-nav-item
|
||||
[icon]="trashFilter.icon"
|
||||
[forceActiveStyles]="activeFilter()?.isDeleted"
|
||||
(click)="applyFilter('trash')"
|
||||
[text]="trashFilter.name | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.isDeleted"
|
||||
[appA11yTitle]="trashFilter.name | i18n"
|
||||
/>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, viewChild, input, inject } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultFilter, CipherStatus, CipherTypeFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-status-filter",
|
||||
templateUrl: "status-filter.component.html",
|
||||
imports: [CommonModule, A11yTitleDirective, NavigationModule, PremiumBadgeComponent, I18nPipe],
|
||||
})
|
||||
export class StatusFilterComponent {
|
||||
private accountService: AccountService = inject(AccountService);
|
||||
private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService);
|
||||
|
||||
protected readonly hideArchive = input(false);
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
protected readonly archiveFilter: CipherTypeFilter = {
|
||||
id: "archive",
|
||||
name: "archiveNoun",
|
||||
type: "archive",
|
||||
icon: "bwi-archive",
|
||||
};
|
||||
protected readonly trashFilter: CipherTypeFilter = {
|
||||
id: "trash",
|
||||
name: "trash",
|
||||
type: "trash",
|
||||
icon: "bwi-trash",
|
||||
};
|
||||
|
||||
protected applyFilter(filterType: CipherStatus) {
|
||||
let filter: CipherTypeFilter = null;
|
||||
if (filterType === "archive") {
|
||||
filter = this.archiveFilter;
|
||||
} else if (filterType === "trash") {
|
||||
filter = this.trashFilter;
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
this.activeFilter().selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(filter, null);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly premiumBadgeComponent = viewChild.required(PremiumBadgeComponent);
|
||||
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
protected canArchive$ = this.userId$.pipe(
|
||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||
);
|
||||
|
||||
protected hasArchivedCiphers$ = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)),
|
||||
),
|
||||
);
|
||||
|
||||
protected async handleArchiveFilter(event: Event) {
|
||||
const [canArchive, hasArchivedCiphers] = await firstValueFrom(
|
||||
combineLatest([this.canArchive$, this.hasArchivedCiphers$]),
|
||||
);
|
||||
|
||||
if (canArchive || hasArchivedCiphers) {
|
||||
this.applyFilter("archive");
|
||||
} else {
|
||||
await this.premiumBadgeComponent().promptForPremium(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<bit-nav-group
|
||||
icon="bwi-filter"
|
||||
(click)="applyAllItemsFilter($event)"
|
||||
[text]="'allItems' | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.selectedCipherTypeNode?.node?.id === 'AllItems'"
|
||||
[appA11yTitle]="'allItems' | i18n"
|
||||
variant="tree"
|
||||
[open]="true"
|
||||
[forceActiveStyles]="activeFilter()?.selectedCipherTypeNode?.node?.id === 'AllItems'"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (typeFilter of typeFilters$ | async; track typeFilter) {
|
||||
<bit-nav-item
|
||||
[icon]="typeFilter.node.icon"
|
||||
[forceActiveStyles]="
|
||||
activeFilter()?.selectedCipherTypeNode.node.type === typeFilter.node.type
|
||||
"
|
||||
(click)="applyTypeFilter($event, typeFilter)"
|
||||
[text]="typeFilter.node.name"
|
||||
[attr.aria-pressed]="
|
||||
activeFilter()?.selectedCipherTypeNode.node.type === typeFilter.node.type
|
||||
"
|
||||
[appA11yTitle]="typeFilter.node.name"
|
||||
/>
|
||||
}
|
||||
</bit-nav-group>
|
||||
@@ -0,0 +1,57 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, input, inject } from "@angular/core";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { NavigationModule, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultFilter, CipherTypeFilter } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-type-filter",
|
||||
templateUrl: "type-filter.component.html",
|
||||
imports: [CommonModule, A11yTitleDirective, NavigationModule, I18nPipe],
|
||||
})
|
||||
export class TypeFilterComponent {
|
||||
private restrictedItemTypesService: RestrictedItemTypesService = inject(
|
||||
RestrictedItemTypesService,
|
||||
);
|
||||
|
||||
protected readonly cipherTypes = input<TreeNode<CipherTypeFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
|
||||
protected applyTypeFilter(event: Event, cipherType: TreeNode<CipherTypeFilter>) {
|
||||
event.stopPropagation();
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedCipherTypeNode = cipherType;
|
||||
}
|
||||
}
|
||||
|
||||
protected applyAllItemsFilter(event: Event) {
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedCipherTypeNode = this.cipherTypes();
|
||||
}
|
||||
}
|
||||
|
||||
protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types from the typeFilters array
|
||||
this.cipherTypes().children.filter(
|
||||
(type) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.allowViewOrgIds.length === 0 &&
|
||||
restrictedType.cipherType === type.node.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
@if (!isLoaded) {
|
||||
<div class="container loading-spinner">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
} @else {
|
||||
<bit-nav-group icon="bwi-vault" [text]="'vault' | i18n" route="new-vault">
|
||||
<app-organization-filter
|
||||
[activeFilter]="activeFilter()"
|
||||
[organizations]="organizations$ | async"
|
||||
[activeOrganizationDataOwnership]="activeOrganizationDataOwnershipPolicy"
|
||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||
/>
|
||||
<app-type-filter [activeFilter]="activeFilter()" [cipherTypes]="cipherTypes$ | async" />
|
||||
<app-status-filter [hideArchive]="!showArchiveVaultFilter" [activeFilter]="activeFilter()" />
|
||||
@if (showCollectionsFilter()) {
|
||||
<bit-nav-group
|
||||
icon="bwi-collection"
|
||||
[text]="'collections' | i18n"
|
||||
variant="tree"
|
||||
[appA11yTitle]="'collections' | i18n"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (collection of (collections$ | async)?.children ?? []; track collection.node.id) {
|
||||
<app-collection-filter [activeFilter]="activeFilter()" [collection]="collection" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
}
|
||||
<bit-nav-group
|
||||
icon="bwi-folder"
|
||||
[text]="'folders' | i18n"
|
||||
variant="tree"
|
||||
[appA11yTitle]="'folders' | i18n"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (folder of (folders$ | async)?.children ?? []; track folder.node.id) {
|
||||
<app-folder-filter
|
||||
[activeFilter]="activeFilter()"
|
||||
[folder]="folder"
|
||||
(onEditFolder)="editFolder($event)"
|
||||
/>
|
||||
}
|
||||
</bit-nav-group>
|
||||
</bit-nav-group>
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject, OnInit, output, computed, signal } from "@angular/core";
|
||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule, DialogService, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
OrganizationFilter,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
VaultFilter,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
AddEditFolderDialogComponent,
|
||||
RoutedVaultFilterBridgeService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../../services/desktop-premium-upgrade-prompt.service";
|
||||
|
||||
import { CollectionFilterComponent } from "./filters/collection-filter.component";
|
||||
import { FolderFilterComponent } from "./filters/folder-filter.component";
|
||||
import { OrganizationFilterComponent } from "./filters/organization-filter.component";
|
||||
import { StatusFilterComponent } from "./filters/status-filter.component";
|
||||
import { TypeFilterComponent } from "./filters/type-filter.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
imports: [
|
||||
I18nPipe,
|
||||
NavigationModule,
|
||||
CommonModule,
|
||||
OrganizationFilterComponent,
|
||||
StatusFilterComponent,
|
||||
TypeFilterComponent,
|
||||
CollectionFilterComponent,
|
||||
FolderFilterComponent,
|
||||
A11yTitleDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: PremiumUpgradePromptService,
|
||||
useClass: DesktopPremiumUpgradePromptService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultFilterComponent implements OnInit {
|
||||
private routedVaultFilterBridgeService = inject(RoutedVaultFilterBridgeService);
|
||||
private vaultFilterService: VaultFilterService = inject(VaultFilterService);
|
||||
private accountService: AccountService = inject(AccountService);
|
||||
private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService);
|
||||
private folderService: FolderService = inject(FolderService);
|
||||
private policyService: PolicyService = inject(PolicyService);
|
||||
private dialogService: DialogService = inject(DialogService);
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
protected readonly activeFilter = signal<VaultFilter | null>(null);
|
||||
protected onFilterChange = output<VaultFilter>();
|
||||
|
||||
private activeUserId: UserId;
|
||||
protected isLoaded = false;
|
||||
protected showArchiveVaultFilter = false;
|
||||
protected activeOrganizationDataOwnershipPolicy: boolean;
|
||||
protected activeSingleOrganizationPolicy: boolean;
|
||||
protected organizations$: Observable<TreeNode<OrganizationFilter>>;
|
||||
protected collections$: Observable<TreeNode<CollectionFilter>>;
|
||||
protected folders$: Observable<TreeNode<FolderFilter>>;
|
||||
protected cipherTypes$: Observable<TreeNode<CipherTypeFilter>>;
|
||||
|
||||
protected readonly showCollectionsFilter = computed<boolean>(() => {
|
||||
return this.organizations$ != null && !this.activeFilter()?.isMyVaultSelected;
|
||||
});
|
||||
|
||||
private async setActivePolicies() {
|
||||
this.activeOrganizationDataOwnershipPolicy = await firstValueFrom(
|
||||
this.policyService.policyAppliesToUser$(
|
||||
PolicyType.OrganizationDataOwnership,
|
||||
this.activeUserId,
|
||||
),
|
||||
);
|
||||
this.activeSingleOrganizationPolicy = await firstValueFrom(
|
||||
this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, this.activeUserId),
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organizations$ = this.vaultFilterService.organizationTree$;
|
||||
if (
|
||||
this.organizations$ != null &&
|
||||
(await firstValueFrom(this.organizations$)).children.length > 0
|
||||
) {
|
||||
await this.setActivePolicies();
|
||||
}
|
||||
this.cipherTypes$ = this.vaultFilterService.cipherTypeTree$;
|
||||
this.folders$ = this.vaultFilterService.folderTree$;
|
||||
this.collections$ = this.vaultFilterService.collectionTree$;
|
||||
|
||||
this.showArchiveVaultFilter = await firstValueFrom(
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||
);
|
||||
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((filter) => {
|
||||
this.activeFilter.set(filter);
|
||||
});
|
||||
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
protected async editFolder(folder: FolderFilter) {
|
||||
if (!this.activeUserId) {
|
||||
return;
|
||||
}
|
||||
const folderView = await firstValueFrom(
|
||||
this.folderService.getDecrypted$(folder.id, this.activeUserId),
|
||||
);
|
||||
|
||||
if (!folderView) {
|
||||
return;
|
||||
}
|
||||
|
||||
AddEditFolderDialogComponent.open(this.dialogService, {
|
||||
editFolderConfig: {
|
||||
folder: {
|
||||
...folderView,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,32 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observable } from "rxjs";
|
||||
import {
|
||||
firstValueFrom,
|
||||
Subject,
|
||||
takeUntil,
|
||||
switchMap,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
from,
|
||||
} from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -60,8 +58,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
ChangeLoginPasswordService,
|
||||
@@ -78,6 +74,9 @@ import {
|
||||
PasswordRepromptService,
|
||||
CipherFormComponent,
|
||||
ArchiveCipherUtilitiesService,
|
||||
VaultFilter,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
@@ -86,8 +85,6 @@ import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-pr
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { AssignCollectionsDesktopComponent } from "../vault/assign-collections";
|
||||
import { ItemFooterComponent } from "../vault/item-footer.component";
|
||||
import { VaultFilterComponent } from "../vault/vault-filter/vault-filter.component";
|
||||
import { VaultFilterModule } from "../vault/vault-filter/vault-filter.module";
|
||||
import { VaultItemsV2Component } from "../vault/vault-items-v2.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
@@ -107,7 +104,6 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
ItemModule,
|
||||
ButtonModule,
|
||||
PremiumBadgeComponent,
|
||||
VaultFilterModule,
|
||||
VaultItemsV2Component,
|
||||
],
|
||||
providers: [
|
||||
@@ -134,21 +130,11 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultComponent<C extends CipherViewLike>
|
||||
implements OnInit, OnDestroy, CopyClickListener
|
||||
{
|
||||
export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(VaultItemsV2Component, { static: true })
|
||||
vaultItemsComponent: VaultItemsV2Component<C> | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(VaultFilterComponent, { static: true })
|
||||
vaultFilterComponent: VaultFilterComponent | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||
folderAddEditModalRef: ViewContainerRef | null = null;
|
||||
vaultItemsComponent: VaultItemsV2Component<CipherView> | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CipherFormComponent)
|
||||
@@ -194,6 +180,7 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
private allOrganizations: Organization[] = [];
|
||||
private allCollections: CollectionView[] = [];
|
||||
private filteredCollections: CollectionView[] = [];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -209,7 +196,6 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
private totpService: TotpService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private searchBarService: SearchBarService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
@@ -220,11 +206,12 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
private folderService: FolderService,
|
||||
private configService: ConfigService,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private policyService: PolicyService,
|
||||
private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService,
|
||||
private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -240,6 +227,14 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
this.userHasPremiumAccess = canAccessPremium;
|
||||
});
|
||||
|
||||
// Subscribe to filter changes from router params via the bridge service
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(
|
||||
switchMap((vaultFilter: VaultFilter) => from(this.applyVaultFilter(vaultFilter))),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone
|
||||
.run(async () => {
|
||||
@@ -267,15 +262,7 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
break;
|
||||
case "syncCompleted":
|
||||
if (this.vaultItemsComponent) {
|
||||
await this.vaultItemsComponent
|
||||
.reload(this.activeFilter.buildFilter())
|
||||
.catch(() => {});
|
||||
}
|
||||
if (this.vaultFilterComponent) {
|
||||
await this.vaultFilterComponent
|
||||
.reloadCollectionsAndFolders(this.activeFilter)
|
||||
.catch(() => {});
|
||||
await this.vaultFilterComponent.reloadOrganizations().catch(() => {});
|
||||
await this.vaultItemsComponent.refresh().catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "modalShown":
|
||||
@@ -377,6 +364,12 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
.subscribe((collections) => {
|
||||
this.allCollections = collections;
|
||||
});
|
||||
|
||||
this.vaultFilterService.filteredCollections$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((collections) => {
|
||||
this.filteredCollections = collections;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -403,19 +396,6 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
this.addType = paramCipherAddType;
|
||||
await this.addCipher(this.addType).catch(() => {});
|
||||
}
|
||||
|
||||
const paramCipherType = toCipherType(params.type);
|
||||
this.activeFilter = new VaultFilter({
|
||||
status: params.deleted ? "trash" : params.favorites ? "favorites" : "all",
|
||||
cipherType: params.action === "add" || paramCipherType == null ? undefined : paramCipherType,
|
||||
selectedFolderId: params.folderId,
|
||||
selectedCollectionId: params.selectedCollectionId,
|
||||
selectedOrganizationId: params.selectedOrganizationId,
|
||||
myVaultOnly: params.myVaultOnly ?? false,
|
||||
});
|
||||
if (this.vaultItemsComponent) {
|
||||
await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,9 +419,7 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
this.collections =
|
||||
this.vaultFilterComponent?.collections?.fullList.filter((c) =>
|
||||
cipher.collectionIds.includes(c.id),
|
||||
) ?? null;
|
||||
this.filteredCollections?.filter((c) => cipher.collectionIds.includes(c.id)) ?? null;
|
||||
this.action = "view";
|
||||
|
||||
await this.go().catch(() => {});
|
||||
@@ -798,19 +776,45 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a filter function to handle CipherListView objects.
|
||||
* CipherListView has a different type structure where type can be a string or object.
|
||||
* This wrapper converts it to CipherView-compatible structure before filtering.
|
||||
*/
|
||||
private wrapFilterForCipherListView(
|
||||
filterFn: (cipher: CipherView) => boolean,
|
||||
): (cipher: CipherViewLike) => boolean {
|
||||
return (cipher: CipherViewLike) => {
|
||||
// For CipherListView, create a proxy object with the correct type property
|
||||
if (CipherViewLikeUtils.isCipherListView(cipher)) {
|
||||
const proxyCipher = {
|
||||
...cipher,
|
||||
type: CipherViewLikeUtils.getType(cipher),
|
||||
// Normalize undefined organizationId to null for filter compatibility
|
||||
organizationId: cipher.organizationId ?? null,
|
||||
// Explicitly include isDeleted and isArchived since they might be getters
|
||||
isDeleted: CipherViewLikeUtils.isDeleted(cipher),
|
||||
isArchived: CipherViewLikeUtils.isArchived(cipher),
|
||||
};
|
||||
return filterFn(proxyCipher as any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.searchBarService.setPlaceholderText(
|
||||
this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)),
|
||||
);
|
||||
this.activeFilter = vaultFilter;
|
||||
await this.vaultItemsComponent
|
||||
?.reload(
|
||||
this.activeFilter.buildFilter(),
|
||||
vaultFilter.status === "trash",
|
||||
vaultFilter.status === "archive",
|
||||
)
|
||||
.catch(() => {});
|
||||
await this.go().catch(() => {});
|
||||
|
||||
const originalFilterFn = this.activeFilter.buildFilter();
|
||||
const wrappedFilterFn = this.wrapFilterForCipherListView(originalFilterFn);
|
||||
|
||||
await this.vaultItemsComponent?.reload(
|
||||
wrappedFilterFn,
|
||||
vaultFilter.isDeleted,
|
||||
vaultFilter.isArchived,
|
||||
);
|
||||
}
|
||||
|
||||
private getAvailableCollections(cipher: CipherView): CollectionView[] {
|
||||
@@ -824,25 +828,25 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
}
|
||||
|
||||
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||
if (vaultFilter.status === "favorites") {
|
||||
if (vaultFilter.isFavorites) {
|
||||
return "searchFavorites";
|
||||
}
|
||||
if (vaultFilter.status === "trash") {
|
||||
if (vaultFilter.isDeleted) {
|
||||
return "searchTrash";
|
||||
}
|
||||
if (vaultFilter.cipherType != null) {
|
||||
return "searchType";
|
||||
}
|
||||
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") {
|
||||
if (vaultFilter.folderId != null && vaultFilter.folderId !== "none") {
|
||||
return "searchFolder";
|
||||
}
|
||||
if (vaultFilter.selectedCollectionId != null) {
|
||||
if (vaultFilter.collectionId != null) {
|
||||
return "searchCollection";
|
||||
}
|
||||
if (vaultFilter.selectedOrganizationId != null) {
|
||||
if (vaultFilter.organizationId != null) {
|
||||
return "searchOrganization";
|
||||
}
|
||||
if (vaultFilter.myVaultOnly) {
|
||||
if (vaultFilter.isMyVaultSelected) {
|
||||
return "searchMyVault";
|
||||
}
|
||||
return "searchVault";
|
||||
@@ -863,23 +867,6 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
if (!folderView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {
|
||||
editFolderConfig: {
|
||||
folder: {
|
||||
...folderView,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (
|
||||
result === AddEditFolderDialogResult.Deleted ||
|
||||
result === AddEditFolderDialogResult.Created
|
||||
) {
|
||||
await this.vaultFilterComponent?.reloadCollectionsAndFolders(this.activeFilter);
|
||||
}
|
||||
}
|
||||
|
||||
/** Refresh the current cipher object */
|
||||
@@ -919,19 +906,13 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
queryParams = {
|
||||
action: this.action,
|
||||
cipherId: this.cipherId,
|
||||
favorites: this.favorites ? true : null,
|
||||
type: this.type,
|
||||
folderId: this.folderId,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
organizationId: this.organizationId,
|
||||
myVaultOnly: this.myVaultOnly,
|
||||
};
|
||||
}
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -966,21 +947,23 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
}
|
||||
|
||||
private prefillCipherFromFilter() {
|
||||
if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) {
|
||||
const collections = this.vaultFilterComponent.collections?.fullList.filter(
|
||||
(c) => c.id === this.activeFilter.selectedCollectionId,
|
||||
if (this.activeFilter.collectionId != null) {
|
||||
const collections = this.filteredCollections?.filter(
|
||||
(c) => c.id === this.activeFilter.collectionId,
|
||||
);
|
||||
if (collections.length > 0) {
|
||||
if (collections?.length > 0) {
|
||||
this.addOrganizationId = collections[0].organizationId;
|
||||
this.addCollectionIds = [this.activeFilter.selectedCollectionId];
|
||||
this.addCollectionIds = [this.activeFilter.collectionId];
|
||||
}
|
||||
} else if (this.activeFilter.selectedOrganizationId) {
|
||||
this.addOrganizationId = this.activeFilter.selectedOrganizationId;
|
||||
} else if (this.activeFilter.organizationId) {
|
||||
this.addOrganizationId = this.activeFilter.organizationId;
|
||||
} else {
|
||||
// clear out organizationId when the user switches to a personal vault filter
|
||||
this.addOrganizationId = null;
|
||||
}
|
||||
this.folderId = this.activeFilter.selectedFolderId;
|
||||
if (this.activeFilter.folderId && this.activeFilter.selectedFolderNode) {
|
||||
this.folderId = this.activeFilter.folderId;
|
||||
}
|
||||
|
||||
if (this.config == null) {
|
||||
return;
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
@@ -32,6 +32,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
|
||||
Reference in New Issue
Block a user