mirror of
https://github.com/bitwarden/browser
synced 2026-01-31 00:33:33 +00:00
migrated vault filters using router params
This commit is contained in:
@@ -41,11 +41,19 @@ import {
|
||||
NewDeviceVerificationComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
VaultFilterServiceAbstraction,
|
||||
VaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
RoutedVaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { DesktopPremiumUpgradePromptService } from "../services/desktop-premium-upgrade-prompt.service";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
import { VaultComponent } from "../vault/app/vault-v3/vault.component";
|
||||
|
||||
@@ -337,6 +345,18 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: DesktopLayoutComponent,
|
||||
canActivate: [authGuard],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
{
|
||||
provide: VaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
},
|
||||
{
|
||||
provide: PremiumUpgradePromptService,
|
||||
useClass: DesktopPremiumUpgradePromptService,
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
path: "new-vault",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<app-side-nav slot="side-nav">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo>
|
||||
|
||||
<app-vault-nav></app-vault-nav>
|
||||
<app-vault-filter></app-vault-filter>
|
||||
<bit-nav-item icon="bwi-send" [text]="'send' | i18n" route="new-sends"></bit-nav-item>
|
||||
</app-side-nav>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PasswordManagerLogo } from "@bitwarden/assets/svg";
|
||||
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { VaultNavComponent } from "../../vault/app/vault-v3/nav/vault-nav.component";
|
||||
import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component";
|
||||
|
||||
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
|
||||
@@ -19,7 +19,7 @@ import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
DesktopSideNavComponent,
|
||||
VaultNavComponent,
|
||||
VaultFilterComponent,
|
||||
],
|
||||
templateUrl: "./desktop-layout.component.html",
|
||||
})
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { SearchBarService } from "../app/layout/search/search-bar.service";
|
||||
|
||||
/**
|
||||
* Service to coordinate vault state, including filter state and folder actions,
|
||||
* between the navigation component and the vault component.
|
||||
*/
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class VaultStateService {
|
||||
private filterChangeSubject = new Subject<VaultFilter>();
|
||||
private addFolderSubject = new Subject<void>();
|
||||
private editFolderSubject = new Subject<string>();
|
||||
|
||||
/**
|
||||
* The currently active vault filter.
|
||||
*/
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
|
||||
/**
|
||||
* Observable stream of vault filter changes.
|
||||
* Subscribe to this to react to filter changes from the navigation.
|
||||
*/
|
||||
readonly filterChange$ = this.filterChangeSubject.asObservable();
|
||||
|
||||
/**
|
||||
* Observable stream of add folder requests.
|
||||
* Subscribe to this to handle folder creation.
|
||||
*/
|
||||
readonly addFolder$ = this.addFolderSubject.asObservable();
|
||||
|
||||
/**
|
||||
* Observable stream of edit folder requests.
|
||||
* Subscribe to this to handle folder editing.
|
||||
* Emits the folder ID to edit.
|
||||
*/
|
||||
readonly editFolder$ = this.editFolderSubject.asObservable();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private searchBarService: SearchBarService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Apply a new vault filter.
|
||||
* This updates the search bar placeholder and notifies all subscribers.
|
||||
*/
|
||||
applyFilter(filter: VaultFilter): void {
|
||||
// Store the active filter
|
||||
this.activeFilter = filter;
|
||||
|
||||
// Update search bar placeholder text based on the filter
|
||||
this.searchBarService.setPlaceholderText(
|
||||
this.i18nService.t(this.calculateSearchBarLocalizationString(filter)),
|
||||
);
|
||||
|
||||
// Emit the filter change to subscribers
|
||||
this.filterChangeSubject.next(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add a new folder.
|
||||
* This will notify subscribers to show the folder creation dialog.
|
||||
*/
|
||||
requestAddFolder(): void {
|
||||
this.addFolderSubject.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to edit an existing folder.
|
||||
* This will notify subscribers to show the folder edit dialog.
|
||||
*/
|
||||
requestEditFolder(folderId: string): void {
|
||||
this.editFolderSubject.next(folderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the appropriate search bar localization string based on the active filter.
|
||||
*/
|
||||
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||
if (vaultFilter.status === "favorites") {
|
||||
return "searchFavorites";
|
||||
}
|
||||
if (vaultFilter.status === "trash") {
|
||||
return "searchTrash";
|
||||
}
|
||||
if (vaultFilter.cipherType != null) {
|
||||
return "searchType";
|
||||
}
|
||||
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") {
|
||||
return "searchFolder";
|
||||
}
|
||||
if (vaultFilter.selectedCollectionId != null) {
|
||||
return "searchCollection";
|
||||
}
|
||||
if (vaultFilter.selectedOrganizationId != null) {
|
||||
return "searchOrganization";
|
||||
}
|
||||
if (vaultFilter.myVaultOnly) {
|
||||
return "searchMyVault";
|
||||
}
|
||||
return "searchVault";
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<bit-nav-group icon="bwi-vault" [text]="'vault' | i18n" route="new-vault">
|
||||
<app-vault-filter
|
||||
class="vault-filters"
|
||||
[activeFilter]="vaultStateService.activeFilter"
|
||||
(onFilterChange)="vaultStateService.applyFilter($event)"
|
||||
(onAddFolder)="vaultStateService.requestAddFolder()"
|
||||
(onEditFolder)="vaultStateService.requestEditFolder($event.id)"
|
||||
></app-vault-filter>
|
||||
</bit-nav-group>
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultStateService } from "../../../../services/vault-state.service";
|
||||
import { VaultFilterModule } from "../vault-filter/vault-filter.module";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-nav",
|
||||
imports: [I18nPipe, NavigationModule, VaultFilterModule],
|
||||
templateUrl: "./vault-nav.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class VaultNavComponent {
|
||||
constructor(protected vaultStateService: VaultStateService) {}
|
||||
}
|
||||
@@ -1,84 +1,23 @@
|
||||
<ng-container *ngIf="show">
|
||||
<div class="filter-heading">
|
||||
<h2>
|
||||
<button
|
||||
type="button"
|
||||
class="no-btn"
|
||||
[attr.aria-expanded]="!isCollapsed(collectionsGrouping)"
|
||||
aria-controls="collection-filters"
|
||||
(click)="toggleCollapse(collectionsGrouping)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed(collectionsGrouping),
|
||||
'bwi-angle-down': !isCollapsed(collectionsGrouping),
|
||||
}"
|
||||
></i>
|
||||
{{ collectionsGrouping.name | i18n }}
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
|
||||
<ng-template #recursiveCollections let-collections>
|
||||
<li
|
||||
class="filter-option"
|
||||
*ngFor="let c of collections"
|
||||
[ngClass]="{ active: c.node.id === activeFilter.selectedCollectionId }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="c.children.length"
|
||||
class="toggle-button"
|
||||
[attr.aria-expanded]="!isCollapsed(c.node)"
|
||||
[attr.aria-controls]="c.node.name + '_children'"
|
||||
(click)="toggleCollapse(c.node)"
|
||||
appA11yTitle="{{ 'toggleCollapse' | i18n }} {{ c.node.name }}"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed(c.node),
|
||||
'bwi-angle-down': !isCollapsed(c.node),
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(c.node)"
|
||||
[attr.aria-pressed]="c.node.id === activeFilter.selectedCollectionId"
|
||||
[title]="c.node.name"
|
||||
>
|
||||
<i
|
||||
*ngIf="c.children.length === 0"
|
||||
[class]="
|
||||
'bwi bwi-fw ' +
|
||||
(c.node.type === DefaultCollectionType ? 'bwi-user' : 'bwi-collection-shared')
|
||||
"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{ c.node.name }}
|
||||
</button>
|
||||
</span>
|
||||
<ul
|
||||
[id]="c.node.name + '_children'"
|
||||
class="nested-filter-options"
|
||||
*ngIf="c.children.length && !isCollapsed(c.node)"
|
||||
>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="recursiveCollections; context: { $implicit: c.children }"
|
||||
>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="recursiveCollections; context: { $implicit: nestedCollections }"
|
||||
>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</ng-container>
|
||||
@if (collection().children.length) {
|
||||
<bit-nav-group
|
||||
[icon]="collection().node.icon"
|
||||
[class.active]="isActive()"
|
||||
[text]="displayName()"
|
||||
[variant]="'tree'"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
>
|
||||
@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"
|
||||
[class.active]="isActive()"
|
||||
[text]="collection().node.name"
|
||||
[variant]="'tree'"
|
||||
[appA11yTitle]="collection().node.name"
|
||||
(click)="applyFilter($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, input, computed } from "@angular/core";
|
||||
|
||||
import { CollectionFilterComponent as BaseCollectionFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/collection-filter.component";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { VaultFilter, CollectionFilter } from "@bitwarden/vault";
|
||||
|
||||
import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports";
|
||||
|
||||
// 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",
|
||||
standalone: false,
|
||||
standalone: true,
|
||||
imports: [...VAULT_FILTER_IMPORTS],
|
||||
})
|
||||
export class CollectionFilterComponent extends BaseCollectionFilterComponent {}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,23 @@
|
||||
<ng-container *ngIf="!hide">
|
||||
<div class="filter-heading">
|
||||
<h2>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
[attr.aria-expanded]="!isCollapsed(foldersGrouping)"
|
||||
aria-controls="folder-filters"
|
||||
(click)="toggleCollapse(foldersGrouping)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed(foldersGrouping),
|
||||
'bwi-angle-down': !isCollapsed(foldersGrouping),
|
||||
}"
|
||||
></i>
|
||||
{{ foldersGrouping.name | i18n }}
|
||||
</button>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="add-button"
|
||||
(click)="addFolder()"
|
||||
appA11yTitle="{{ 'addFolder' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul id="folder-filters" class="filter-options" *ngIf="!isCollapsed(foldersGrouping)">
|
||||
<ng-template #recursiveFolders let-folders>
|
||||
<li
|
||||
*ngFor="let f of folders"
|
||||
[ngClass]="{
|
||||
active: f.node.id === activeFilter.selectedFolderId && activeFilter.selectedFolder,
|
||||
}"
|
||||
class="filter-option"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
*ngIf="f.children.length"
|
||||
(click)="toggleCollapse(f.node)"
|
||||
appA11yTitle="{{ 'toggleCollapse' | i18n }} {{ f.node.name }}"
|
||||
[attr.aria-expanded]="!isCollapsed(f.node)"
|
||||
[attr.aria-controls]="f.node.name + '_children'"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed(f.node),
|
||||
'bwi-angle-down': !isCollapsed(f.node),
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(f.node)"
|
||||
[attr.aria-pressed]="
|
||||
activeFilter.selectedFolder && f.node.id === activeFilter.selectedFolderId
|
||||
"
|
||||
>
|
||||
<i *ngIf="f.children.length === 0" class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||
{{ f.node.name }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="edit-button"
|
||||
*ngIf="f.node.id"
|
||||
(click)="editFolder(f.node)"
|
||||
appA11yTitle="{{ 'editFolder' | i18n }}: {{ f.node.name }}"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
<ul
|
||||
[id]="f.node.name + '_children'"
|
||||
class="nested-filter-options"
|
||||
*ngIf="f.children.length && !isCollapsed(f.node)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="recursiveFolders; context: { $implicit: f.children }">
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="recursiveFolders; context: { $implicit: nestedFolders }"
|
||||
></ng-container>
|
||||
</ul>
|
||||
</ng-container>
|
||||
@if (folder().children.length) {
|
||||
<bit-nav-group
|
||||
[icon]="folder().node.icon"
|
||||
[class.active]="isActive()"
|
||||
[text]="displayName()"
|
||||
[variant]="'tree'"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
>
|
||||
@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"
|
||||
[class.active]="isActive()"
|
||||
[text]="displayName()"
|
||||
[variant]="'tree'"
|
||||
[appA11yTitle]="displayName()"
|
||||
(click)="applyFilter($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, input, computed } from "@angular/core";
|
||||
|
||||
import { FolderFilterComponent as BaseFolderFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/folder-filter.component";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { VaultFilter, FolderFilter } from "@bitwarden/vault";
|
||||
|
||||
import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports";
|
||||
|
||||
// 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",
|
||||
standalone: false,
|
||||
standalone: true,
|
||||
imports: [...VAULT_FILTER_IMPORTS],
|
||||
})
|
||||
export class FolderFilterComponent extends BaseFolderFilterComponent {}
|
||||
export class FolderFilterComponent {
|
||||
protected readonly folder = input<TreeNode<FolderFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +1,38 @@
|
||||
<ng-container *ngIf="show">
|
||||
<ng-container [ngSwitch]="displayMode">
|
||||
<ng-container *ngSwitchCase="'organizationDataOwnershipPolicy'">
|
||||
<div class="filter-heading" [ngClass]="{ active: !hasActiveFilter }">
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
appA11yTitle="{{ 'toggleCollapse' | i18n }} {{ organizationGrouping.name | i18n }}"
|
||||
(click)="toggleCollapse()"
|
||||
[attr.aria-expanded]="!isCollapsed"
|
||||
aria-controls="organization-filters"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed,
|
||||
'bwi-angle-down': !isCollapsed,
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<h2>
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="clearFilter()"
|
||||
[attr.aria-pressed]="!hasActiveFilter"
|
||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organizationGrouping.name | i18n }}"
|
||||
>
|
||||
{{ organizationGrouping.name | i18n }}
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options no-margin">
|
||||
<li
|
||||
class="filter-option"
|
||||
*ngFor="let organization of organizations"
|
||||
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyOrganizationFilter(organization)"
|
||||
[attr.aria-pressed]="activeFilter.myVaultOnly"
|
||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organization.name }}"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organization.name }}
|
||||
</button>
|
||||
<span *ngIf="!organization.enabled" 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>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<div class="filter-heading" [ngClass]="{ active: !hasActiveFilter }">
|
||||
<button
|
||||
type="button"
|
||||
appA11yTitle="{{ 'toggleCollapse' | i18n }} {{ organizationGrouping.name | i18n }}"
|
||||
(click)="toggleCollapse()"
|
||||
[attr.aria-expanded]="!isCollapsed"
|
||||
aria-controls="organization-filters"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed,
|
||||
'bwi-angle-down': !isCollapsed,
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<h2>
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="clearFilter()"
|
||||
[attr.aria-pressed]="!hasActiveFilter"
|
||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organizationGrouping.name | i18n }}"
|
||||
>
|
||||
{{ organizationGrouping.name | i18n }}
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options no-margin">
|
||||
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyMyVaultFilter()"
|
||||
[attr.aria-pressed]="activeFilter.myVaultOnly"
|
||||
appA11yTitle="{{ 'vault' | i18n }}: {{ 'myVault' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
||||
{{ "myVault" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
*ngFor="let organization of organizations"
|
||||
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyOrganizationFilter(organization)"
|
||||
appA11yTitle="{{ 'vault' | i18n }}: {{ organization.name }} {{
|
||||
organization.enabled ? '' : '(' + ('organizationIsDisabled' | i18n) + ')'
|
||||
}}"
|
||||
[attr.aria-pressed]="activeFilter.selectedOrganizationId === organization.id"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organization.name }}
|
||||
</button>
|
||||
<span *ngIf="!organization.enabled" 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>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<hr />
|
||||
</ng-container>
|
||||
@if (show()) {
|
||||
@for (organization of organizations().children ?? []; track organization.node.id) {
|
||||
@if (getOrgCollections(organization.node.id)?.children?.length > 0) {
|
||||
<bit-nav-group
|
||||
[icon]="organization.node.icon"
|
||||
[class.active]="organization.node.id === activeFilter()?.organizationId"
|
||||
[text]="organization.node.name"
|
||||
[variant]="'tree'"
|
||||
[appA11yTitle]="organization.node.name"
|
||||
(click)="applyFilter(organization)"
|
||||
>
|
||||
@if (!hideCollections() && collections() != null) {
|
||||
@for (c of getOrgCollections(organization.node.id)?.children ?? []; track c.node.id) {
|
||||
<app-collection-filter [activeFilter]="activeFilter()" [collection]="c" />
|
||||
}
|
||||
}
|
||||
</bit-nav-group>
|
||||
} @else {
|
||||
<bit-nav-item
|
||||
[icon]="organization.node.icon"
|
||||
[class.active]="organization.node.id === activeFilter()?.organizationId"
|
||||
[text]="organization.node.name"
|
||||
[variant]="'tree'"
|
||||
[appA11yTitle]="organization.node.name"
|
||||
(click)="applyFilter(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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,102 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, computed, input, inject } from "@angular/core";
|
||||
|
||||
import { OrganizationFilterComponent as BaseOrganizationFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/organization-filter.component";
|
||||
import { DisplayMode } from "@bitwarden/angular/vault/vault-filter/models/display-mode";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { OrganizationFilter, VaultFilter, CollectionFilter } from "@bitwarden/vault";
|
||||
|
||||
import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports";
|
||||
|
||||
import { CollectionFilterComponent } from "./collection-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-organization-filter",
|
||||
templateUrl: "organization-filter.component.html",
|
||||
standalone: false,
|
||||
standalone: true,
|
||||
imports: [...VAULT_FILTER_IMPORTS, CollectionFilterComponent],
|
||||
})
|
||||
export class OrganizationFilterComponent extends BaseOrganizationFilterComponent {
|
||||
get show() {
|
||||
export class OrganizationFilterComponent {
|
||||
private toastService: ToastService = inject(ToastService);
|
||||
private i18nService: I18nService = inject(I18nService);
|
||||
|
||||
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 hideCollections = input(false);
|
||||
protected readonly collections = input<TreeNode<CollectionFilter>>();
|
||||
|
||||
protected readonly show = computed(() => {
|
||||
const hiddenDisplayModes: DisplayMode[] = [
|
||||
"singleOrganizationAndOrganizatonDataOwnershipPolicies",
|
||||
];
|
||||
return (
|
||||
!this.hide &&
|
||||
this.organizations.length > 0 &&
|
||||
hiddenDisplayModes.indexOf(this.displayMode) === -1
|
||||
!this.hide() &&
|
||||
this.organizations()?.children.length > 0 &&
|
||||
hiddenDisplayModes.indexOf(this.displayMode()) === -1
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
async applyOrganizationFilter(organization: Organization) {
|
||||
if (organization.enabled) {
|
||||
//proceed with default behaviour for enabled organizations
|
||||
// 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
|
||||
super.applyOrganizationFilter(organization);
|
||||
} else {
|
||||
return displayMode;
|
||||
});
|
||||
|
||||
protected applyFilter(organization: TreeNode<OrganizationFilter>) {
|
||||
if (!organization.node.enabled) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("disabledOrganizationFilterError"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedOrganizationNode = organization;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly collectionsByOrganization = computed(() => {
|
||||
const collections = this.collections();
|
||||
const map = new Map<OrganizationId, TreeNode<CollectionFilter>>();
|
||||
const orgs = this.organizations()?.children;
|
||||
|
||||
if (!collections || !orgs) {
|
||||
return map;
|
||||
}
|
||||
|
||||
for (const org of orgs) {
|
||||
const filteredCollections = collections.children.filter(
|
||||
(node) => node.node.organizationId === org.node.id,
|
||||
);
|
||||
|
||||
const headNode = new TreeNode<CollectionFilter>(collections.node, null);
|
||||
headNode.children = filteredCollections;
|
||||
map.set(org.node.id, headNode);
|
||||
}
|
||||
|
||||
return map;
|
||||
});
|
||||
|
||||
protected getOrgCollections(organizationId: OrganizationId): TreeNode<CollectionFilter> {
|
||||
return this.collectionsByOrganization().get(organizationId) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,25 @@
|
||||
<ng-container *ngIf="show">
|
||||
<h2 class="sr-only">{{ "filters" | i18n }}</h2>
|
||||
<ul class="filter-options">
|
||||
<li class="filter-option" [ngClass]="{ active: activeFilter.status === 'all' }">
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter('all')"
|
||||
[attr.aria-pressed]="activeFilter.status === 'all'"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i> {{ "allItems" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
*ngIf="!hideFavorites"
|
||||
[ngClass]="{ active: activeFilter.status === 'favorites' }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter('favorites')"
|
||||
[attr.aria-pressed]="activeFilter.status === 'favorites'"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i> {{ "favorites" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="filter-option tw-flex tw-items-center tw-gap-2 [&>span]:tw-w-min"
|
||||
[ngClass]="{ active: activeFilter.status === 'archive' }"
|
||||
*ngIf="!hideArchive"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="handleArchiveFilter($event)"
|
||||
[attr.aria-pressed]="activeFilter.status === 'archive'"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i> {{ "archiveNoun" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
@if (!(canArchive$ | async)) {
|
||||
<app-premium-badge></app-premium-badge>
|
||||
}
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
*ngIf="!hideTrash"
|
||||
[ngClass]="{ active: activeFilter.status === 'trash' }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter('trash')"
|
||||
[attr.aria-pressed]="activeFilter.status === 'trash'"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i> {{ "trash" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
@if (show()) {
|
||||
@if (!hideArchive()) {
|
||||
<bit-nav-item
|
||||
[icon]="archiveFilter.icon"
|
||||
[class.active]="activeFilter()?.isArchived"
|
||||
(click)="handleArchiveFilter($event)"
|
||||
[text]="archiveFilter.name | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.isArchived"
|
||||
[appA11yTitle]="archiveFilter.name | i18n"
|
||||
/>
|
||||
@if (!(canArchive$ | async)) {
|
||||
<app-premium-badge />
|
||||
}
|
||||
}
|
||||
@if (!hideTrash()) {
|
||||
<bit-nav-item
|
||||
[icon]="trashFilter.icon"
|
||||
[class.active]="activeFilter()?.isDeleted"
|
||||
(click)="applyFilter('trash')"
|
||||
[text]="trashFilter.name | i18n"
|
||||
[attr.aria-pressed]="activeFilter()?.isDeleted"
|
||||
[appA11yTitle]="trashFilter.name | i18n"
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { StatusFilterComponent } from "./status-filter.component";
|
||||
|
||||
describe("StatusFilterComponent", () => {
|
||||
let component: StatusFilterComponent;
|
||||
let fixture: ComponentFixture<StatusFilterComponent>;
|
||||
let cipherArchiveService: jest.Mocked<CipherArchiveService>;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const event = new Event("click");
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
cipherArchiveService = mock<CipherArchiveService>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [StatusFilterComponent],
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: CipherArchiveService, useValue: cipherArchiveService },
|
||||
{ provide: PremiumUpgradePromptService, useValue: mock<PremiumUpgradePromptService>() },
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: mock<BillingAccountProfileStateService>(),
|
||||
},
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
imports: [JslibModule, PremiumBadgeComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(StatusFilterComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.activeFilter = new VaultFilter();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("handleArchiveFilter", () => {
|
||||
const applyFilter = jest.fn();
|
||||
let promptForPremiumSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
applyFilter.mockClear();
|
||||
component["applyFilter"] = applyFilter;
|
||||
|
||||
promptForPremiumSpy = jest.spyOn(component["premiumBadgeComponent"]()!, "promptForPremium");
|
||||
});
|
||||
|
||||
it("should apply archive filter when userCanArchive returns true", async () => {
|
||||
cipherArchiveService.userCanArchive$.mockReturnValue(of(true));
|
||||
cipherArchiveService.archivedCiphers$.mockReturnValue(of([]));
|
||||
|
||||
await component["handleArchiveFilter"](event);
|
||||
|
||||
expect(applyFilter).toHaveBeenCalledWith("archive");
|
||||
expect(promptForPremiumSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should apply archive filter when userCanArchive returns false but hasArchivedCiphers is true", async () => {
|
||||
const mockCipher = new CipherView();
|
||||
mockCipher.id = "test-id";
|
||||
|
||||
cipherArchiveService.userCanArchive$.mockReturnValue(of(false));
|
||||
cipherArchiveService.archivedCiphers$.mockReturnValue(of([mockCipher]));
|
||||
|
||||
await component["handleArchiveFilter"](event);
|
||||
|
||||
expect(applyFilter).toHaveBeenCalledWith("archive");
|
||||
expect(promptForPremiumSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should prompt for premium when userCanArchive returns false and hasArchivedCiphers is false", async () => {
|
||||
cipherArchiveService.userCanArchive$.mockReturnValue(of(false));
|
||||
cipherArchiveService.archivedCiphers$.mockReturnValue(of([]));
|
||||
|
||||
await component["handleArchiveFilter"](event);
|
||||
|
||||
expect(applyFilter).not.toHaveBeenCalled();
|
||||
expect(promptForPremiumSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,60 @@
|
||||
import { Component, viewChild } from "@angular/core";
|
||||
import { Component, viewChild, input, inject, computed } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { StatusFilterComponent as BaseStatusFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/status-filter.component";
|
||||
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 { VaultFilter, CipherStatus, CipherTypeFilter } from "@bitwarden/vault";
|
||||
|
||||
import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports";
|
||||
|
||||
// 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",
|
||||
standalone: false,
|
||||
standalone: true,
|
||||
imports: [...VAULT_FILTER_IMPORTS, PremiumBadgeComponent],
|
||||
})
|
||||
export class StatusFilterComponent extends BaseStatusFilterComponent {
|
||||
export class StatusFilterComponent {
|
||||
private accountService: AccountService = inject(AccountService);
|
||||
private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService);
|
||||
|
||||
protected readonly hideTrash = input(false);
|
||||
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 readonly show = computed(() => {
|
||||
return !(this.hideTrash() && this.hideArchive());
|
||||
});
|
||||
|
||||
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(PremiumBadgeComponent);
|
||||
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
@@ -28,13 +68,6 @@ export class StatusFilterComponent extends BaseStatusFilterComponent {
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async handleArchiveFilter(event: Event) {
|
||||
const [canArchive, hasArchivedCiphers] = await firstValueFrom(
|
||||
combineLatest([this.canArchive$, this.hasArchivedCiphers$]),
|
||||
|
||||
@@ -1,39 +1,10 @@
|
||||
<div class="filter-heading">
|
||||
<h2>
|
||||
<button
|
||||
type="button"
|
||||
class="no-btn"
|
||||
(click)="toggleCollapse()"
|
||||
[attr.aria-expanded]="!isCollapsed"
|
||||
aria-controls="type-filters"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed,
|
||||
'bwi-angle-down': !isCollapsed,
|
||||
}"
|
||||
></i>
|
||||
{{ typesNode.name | i18n }}
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||
@for (typeFilter of typeFilters$ | async; track typeFilter) {
|
||||
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === typeFilter.type }">
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(typeFilter.type)"
|
||||
[attr.aria-pressed]="activeFilter.cipherType === typeFilter.type"
|
||||
>
|
||||
<i class="bwi bwi-fw {{ typeFilter.icon }}" aria-hidden="true"></i> {{
|
||||
typeFilter.labelKey | i18n
|
||||
}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@for (typeFilter of typeFilters$ | async; track typeFilter) {
|
||||
<bit-nav-item
|
||||
[icon]="typeFilter.node.icon"
|
||||
[class.active]="activeFilter()?.cipherType === typeFilter.node.type"
|
||||
(click)="applyFilter(typeFilter)"
|
||||
[text]="typeFilter.node.name"
|
||||
[attr.aria-pressed]="activeFilter()?.cipherType === typeFilter.node.type"
|
||||
[appA11yTitle]="typeFilter.node.name"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,34 +1,48 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, input, inject } from "@angular/core";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { VaultFilter, CipherTypeFilter } from "@bitwarden/vault";
|
||||
|
||||
import { VAULT_FILTER_IMPORTS } from "../shared-filter-imports";
|
||||
|
||||
// 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",
|
||||
standalone: false,
|
||||
standalone: true,
|
||||
imports: [...VAULT_FILTER_IMPORTS],
|
||||
})
|
||||
export class TypeFilterComponent extends BaseTypeFilterComponent {
|
||||
export class TypeFilterComponent {
|
||||
private restrictedItemTypesService: RestrictedItemTypesService = inject(
|
||||
RestrictedItemTypesService,
|
||||
);
|
||||
|
||||
protected readonly cipherTypes = input<TreeNode<CipherTypeFilter>>();
|
||||
protected readonly activeFilter = input<VaultFilter>();
|
||||
|
||||
protected applyFilter(cipherType: TreeNode<CipherTypeFilter>) {
|
||||
const filter = this.activeFilter();
|
||||
|
||||
if (filter) {
|
||||
filter.selectedCipherTypeNode = cipherType;
|
||||
}
|
||||
}
|
||||
|
||||
protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types from the typeFilters array
|
||||
CIPHER_MENU_ITEMS.filter(
|
||||
(typeFilter) =>
|
||||
this.cipherTypes().children.filter(
|
||||
(type) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.allowViewOrgIds.length === 0 &&
|
||||
restrictedType.cipherType === typeFilter.type,
|
||||
restrictedType.cipherType === type.node.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
constructor(private restrictedItemTypesService: RestrictedItemTypesService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
|
||||
/**
|
||||
* Common imports shared across all vault filter components.
|
||||
*/
|
||||
export const VAULT_FILTER_IMPORTS = [CommonModule, JslibModule, NavigationModule] as const;
|
||||
@@ -1,51 +1,30 @@
|
||||
<div class="container loading-spinner" *ngIf="!isLoaded">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ng-container *ngIf="isLoaded">
|
||||
<app-organization-filter
|
||||
class="filter"
|
||||
[hide]="hideOrganizations"
|
||||
[activeFilter]="activeFilter"
|
||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||
[organizations]="organizations"
|
||||
[activeOrganizationDataOwnership]="activeOrganizationDataOwnershipPolicy"
|
||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
></app-organization-filter>
|
||||
<app-status-filter
|
||||
class="filter"
|
||||
[hideFavorites]="hideFavorites"
|
||||
[hideTrash]="hideTrash"
|
||||
[hideArchive]="!showArchiveVaultFilter"
|
||||
[activeFilter]="activeFilter"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
></app-status-filter>
|
||||
<app-type-filter
|
||||
class="filter"
|
||||
[activeFilter]="activeFilter"
|
||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
></app-type-filter>
|
||||
<app-folder-filter
|
||||
class="filter"
|
||||
[hide]="hideFolders"
|
||||
[activeFilter]="activeFilter"
|
||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||
[folderNodes]="folders$ | async"
|
||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event)"
|
||||
></app-folder-filter>
|
||||
<app-collection-filter
|
||||
class="filter"
|
||||
[hide]="hideCollections"
|
||||
[activeFilter]="activeFilter"
|
||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||
[collectionNodes]="collections"
|
||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
></app-collection-filter>
|
||||
</ng-container>
|
||||
@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
|
||||
[hide]="hideOrganizations()"
|
||||
[activeFilter]="activeFilter"
|
||||
[organizations]="organizations$ | async"
|
||||
[activeOrganizationDataOwnership]="activeOrganizationDataOwnershipPolicy"
|
||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||
[hideCollections]="hideCollections()"
|
||||
[collections]="collections$ | async"
|
||||
/>
|
||||
<app-type-filter [activeFilter]="activeFilter" [cipherTypes]="cipherTypes$ | async" />
|
||||
<app-status-filter
|
||||
[hideTrash]="hideTrash()"
|
||||
[hideArchive]="!showArchiveVaultFilter"
|
||||
[activeFilter]="activeFilter"
|
||||
/>
|
||||
@if (!hideFolders()) {
|
||||
<bit-nav-group [icon]="'bwi-folder'" [text]="'folders' | i18n" [variant]="'tree'">
|
||||
@for (folder of (folders$ | async)?.children ?? []; track folder.node.id) {
|
||||
<app-folder-filter [activeFilter]="activeFilter" [folder]="folder" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
}
|
||||
</bit-nav-group>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,109 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject, OnInit, input, output } from "@angular/core";
|
||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/vault-filter.component";
|
||||
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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
OrganizationFilter,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
VaultFilter,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
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",
|
||||
standalone: false,
|
||||
standalone: true,
|
||||
imports: [
|
||||
I18nPipe,
|
||||
NavigationModule,
|
||||
CommonModule,
|
||||
OrganizationFilterComponent,
|
||||
StatusFilterComponent,
|
||||
TypeFilterComponent,
|
||||
FolderFilterComponent,
|
||||
],
|
||||
})
|
||||
export class VaultFilterComponent extends BaseVaultFilterComponent {}
|
||||
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 policyService: PolicyService = inject(PolicyService);
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
protected activeFilter: VaultFilter;
|
||||
protected readonly hideFolders = input(false);
|
||||
protected readonly hideCollections = input(false);
|
||||
protected readonly hideFavorites = input(false);
|
||||
protected readonly hideTrash = input(false);
|
||||
protected readonly hideOrganizations = input(false);
|
||||
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>>;
|
||||
|
||||
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$(),
|
||||
);
|
||||
|
||||
// Subscribe to the active filter from the bridge service
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((filter) => {
|
||||
this.activeFilter = filter;
|
||||
});
|
||||
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/vault/abstractions/deprecated-vault-filter.service";
|
||||
import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.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";
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, JslibModule, PremiumBadgeComponent],
|
||||
declarations: [
|
||||
VaultFilterComponent,
|
||||
CollectionFilterComponent,
|
||||
FolderFilterComponent,
|
||||
OrganizationFilterComponent,
|
||||
StatusFilterComponent,
|
||||
TypeFilterComponent,
|
||||
],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: DeprecatedVaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultFilterModule {}
|
||||
@@ -9,15 +9,21 @@ import {
|
||||
ViewContainerRef,
|
||||
} 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 { 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";
|
||||
@@ -28,7 +34,6 @@ 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 +65,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
ChangeLoginPasswordService,
|
||||
@@ -78,17 +81,17 @@ import {
|
||||
PasswordRepromptService,
|
||||
CipherFormComponent,
|
||||
ArchiveCipherUtilitiesService,
|
||||
VaultFilter,
|
||||
RoutedVaultFilterBridgeService,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service";
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
|
||||
import { VaultStateService } from "../../../services/vault-state.service";
|
||||
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";
|
||||
@@ -108,7 +111,6 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
ItemModule,
|
||||
ButtonModule,
|
||||
PremiumBadgeComponent,
|
||||
VaultFilterModule,
|
||||
VaultItemsV2Component,
|
||||
],
|
||||
providers: [
|
||||
@@ -135,17 +137,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;
|
||||
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("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||
@@ -195,6 +191,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,
|
||||
@@ -210,7 +207,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,
|
||||
@@ -221,12 +217,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 vaultStateService: VaultStateService,
|
||||
private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -242,26 +238,10 @@ export class VaultComponent<C extends CipherViewLike>
|
||||
this.userHasPremiumAccess = canAccessPremium;
|
||||
});
|
||||
|
||||
// Subscribe to filter changes from VaultNavComponent
|
||||
this.vaultStateService.filterChange$
|
||||
// Subscribe to filter changes from router params via the bridge service
|
||||
this.routedVaultFilterBridgeService.activeFilter$
|
||||
.pipe(
|
||||
switchMap((vaultFilter: VaultFilter) => this.applyVaultFilter(vaultFilter)),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Subscribe to add folder requests from VaultNavComponent
|
||||
this.vaultStateService.addFolder$
|
||||
.pipe(
|
||||
switchMap(() => this.addFolder()),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Subscribe to edit folder requests from VaultNavComponent
|
||||
this.vaultStateService.editFolder$
|
||||
.pipe(
|
||||
switchMap((folderId: string) => this.editFolder(folderId)),
|
||||
switchMap((vaultFilter: VaultFilter) => from(this.applyVaultFilter(vaultFilter))),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
@@ -293,15 +273,12 @@ 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(() => {});
|
||||
// const filterFn = this.wrapFilterForCipherListView(
|
||||
// this.activeFilter.buildFilter(),
|
||||
// );
|
||||
// await this.vaultItemsComponent.reload(filterFn).catch(() => {});
|
||||
const filter = this.activeFilter.buildFilter();
|
||||
await this.vaultItemsComponent.reload(filter).catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "modalShown":
|
||||
@@ -403,6 +380,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() {
|
||||
@@ -429,19 +412,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(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,9 +435,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(() => {});
|
||||
@@ -824,19 +792,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[] {
|
||||
@@ -850,25 +844,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";
|
||||
@@ -889,23 +883,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 */
|
||||
@@ -992,22 +969,22 @@ 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;
|
||||
}
|
||||
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
||||
this.folderId = this.activeFilter.selectedFolderId;
|
||||
if (this.activeFilter.folderId && this.activeFilter.selectedFolderNode) {
|
||||
this.folderId = this.activeFilter.folderId;
|
||||
}
|
||||
|
||||
if (this.config == null) {
|
||||
|
||||
Reference in New Issue
Block a user