mirror of
https://github.com/bitwarden/browser
synced 2026-01-31 08:43:54 +00:00
Duplicated '/vault-filters' into '/vault-v3'
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<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>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { CollectionFilterComponent as BaseCollectionFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/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-collection-filter",
|
||||
templateUrl: "collection-filter.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class CollectionFilterComponent extends BaseCollectionFilterComponent {}
|
||||
@@ -0,0 +1,94 @@
|
||||
<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>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { FolderFilterComponent as BaseFolderFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/folder-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-folder-filter",
|
||||
templateUrl: "folder-filter.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class FolderFilterComponent extends BaseFolderFilterComponent {}
|
||||
@@ -0,0 +1,140 @@
|
||||
<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>
|
||||
@@ -0,0 +1,53 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } 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 { ToastService } from "@bitwarden/components";
|
||||
|
||||
// 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,
|
||||
})
|
||||
export class OrganizationFilterComponent extends BaseOrganizationFilterComponent {
|
||||
get show() {
|
||||
const hiddenDisplayModes: DisplayMode[] = [
|
||||
"singleOrganizationAndOrganizatonDataOwnershipPolicies",
|
||||
];
|
||||
return (
|
||||
!this.hide &&
|
||||
this.organizations.length > 0 &&
|
||||
hiddenDisplayModes.indexOf(this.displayMode) === -1
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
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 {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("disabledOrganizationFilterError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<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>
|
||||
@@ -0,0 +1,98 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Component, viewChild } 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";
|
||||
|
||||
// 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,
|
||||
})
|
||||
export class StatusFilterComponent extends BaseStatusFilterComponent {
|
||||
private readonly premiumBadgeComponent = viewChild(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)),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async handleArchiveFilter(event: Event) {
|
||||
const [canArchive, hasArchivedCiphers] = await firstValueFrom(
|
||||
combineLatest([this.canArchive$, this.hasArchivedCiphers$]),
|
||||
);
|
||||
|
||||
if (canArchive || hasArchivedCiphers) {
|
||||
this.applyFilter("archive");
|
||||
} else if (this.premiumBadgeComponent()) {
|
||||
// The `premiumBadgeComponent` should always be defined here, adding the
|
||||
// if to satisfy TypeScript.
|
||||
await this.premiumBadgeComponent().promptForPremium(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<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>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
|
||||
// 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,
|
||||
})
|
||||
export class TypeFilterComponent extends BaseTypeFilterComponent {
|
||||
protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types from the typeFilters array
|
||||
CIPHER_MENU_ITEMS.filter(
|
||||
(typeFilter) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.allowViewOrgIds.length === 0 &&
|
||||
restrictedType.cipherType === typeFilter.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
constructor(private restrictedItemTypesService: RestrictedItemTypesService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<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>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/vault-filter.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultFilterComponent extends BaseVaultFilterComponent {}
|
||||
@@ -0,0 +1,34 @@
|
||||
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 {}
|
||||
Reference in New Issue
Block a user