diff --git a/libs/tools/send/send-ui/jest.config.js b/libs/tools/send/send-ui/jest.config.js index 100075fc7a7..97951fc6035 100644 --- a/libs/tools/send/send-ui/jest.config.js +++ b/libs/tools/send/send-ui/jest.config.js @@ -5,6 +5,10 @@ const { compilerOptions } = require("../../../shared/tsconfig.libs"); /** @type {import('jest').Config} */ module.exports = { testMatch: ["**/+(*.)+(spec).+(ts)"], + roots: ["/libs/tools/send/send-ui/src"], + transform: { + "^.+\\.(ts|tsx)$": "ts-jest", + }, preset: "ts-jest", testEnvironment: "jsdom", moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { diff --git a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html new file mode 100644 index 00000000000..e74e2f05627 --- /dev/null +++ b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html @@ -0,0 +1,11 @@ +
+
+ + +
+
diff --git a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts new file mode 100644 index 00000000000..9a4cbc6f128 --- /dev/null +++ b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts @@ -0,0 +1,25 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ChipSelectComponent } from "@bitwarden/components"; + +import { SendListFiltersService } from "../services/send-list-filters.service"; + +@Component({ + standalone: true, + selector: "app-vault-list-filters", + templateUrl: "./send-list-filters.component.html", + imports: [CommonModule, JslibModule, ChipSelectComponent, ReactiveFormsModule], +}) +export class VaultListFiltersComponent implements OnDestroy { + protected filterForm = this.sendListFiltersService.filterForm; + protected sendTypes = this.sendListFiltersService.sendTypes; + + constructor(private sendListFiltersService: SendListFiltersService) {} + + ngOnDestroy(): void { + this.sendListFiltersService.resetFilterForm(); + } +} diff --git a/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts b/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts new file mode 100644 index 00000000000..afe831aeb1e --- /dev/null +++ b/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts @@ -0,0 +1,72 @@ +import { TestBed } from "@angular/core/testing"; +import { FormBuilder } from "@angular/forms"; +import { BehaviorSubject } from "rxjs"; + +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { Send } from "@bitwarden/common/tools/send/models/domain/send"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; + +import { SendListFiltersService } from "./send-list-filters.service"; + +describe("SendListFiltersService", () => { + let service: SendListFiltersService; + const sends$ = new BehaviorSubject({}); + const policyAppliesToActiveUser$ = new BehaviorSubject(false); + + const sendService = { + sends$, + } as unknown as SendService; + + const i18nService = { + t: (key: string) => key, + } as I18nService; + + const policyService = { + policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$), + }; + + beforeEach(() => { + policyAppliesToActiveUser$.next(false); + policyService.policyAppliesToActiveUser$.mockClear(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: SendService, + useValue: sendService, + }, + { + provide: I18nService, + useValue: i18nService, + }, + { + provide: PolicyService, + useValue: policyService, + }, + { provide: FormBuilder, useClass: FormBuilder }, + ], + }); + + service = TestBed.inject(SendListFiltersService); + }); + + it("returns all send types", () => { + expect(service.sendTypes.map((c) => c.value)).toEqual([SendType.File, SendType.Text]); + }); + + it("filters by sendType", (done) => { + const sends = [ + { type: SendType.File }, + { type: SendType.Text }, + { type: SendType.File }, + ] as Send[]; + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(sends)).toEqual([sends[1]]); + done(); + }); + + service.filterForm.patchValue({ sendType: SendType.Text }); + }); +}); diff --git a/libs/tools/send/send-ui/src/services/send-list-filters.service.ts b/libs/tools/send/send-ui/src/services/send-list-filters.service.ts new file mode 100644 index 00000000000..eb12477da13 --- /dev/null +++ b/libs/tools/send/send-ui/src/services/send-list-filters.service.ts @@ -0,0 +1,113 @@ +import { Injectable } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { map, Observable, startWith } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { Send } from "@bitwarden/common/tools/send/models/domain/send"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service"; +import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { ChipSelectOption } from "@bitwarden/components"; + +export type SendListFilter = { + sendType: SendType | null; +}; + +const INITIAL_FILTERS: SendListFilter = { + sendType: null, +}; + +@Injectable({ + providedIn: "root", +}) +export class SendListFiltersService { + /** + * UI form for all filters + */ + filterForm = this.formBuilder.group(INITIAL_FILTERS); + + /** + * Observable for `filterForm` value + */ + filters$ = this.filterForm.valueChanges.pipe( + startWith(INITIAL_FILTERS), + ) as Observable; + + /** + * All available sends + **/ + + private sends: Send[] = []; + + private sends$: Observable = this.sendService.sends$.pipe( + map((sends) => { + this.sends = sends; + return sends; + }), + ); + + constructor( + private i18nService: I18nService, + private formBuilder: FormBuilder, + private sendService: SendService, + ) {} + + /** + * Observable whose value is a function that filters an array of `Send` objects based on the current filters + */ + filterFunction$: Observable<(send: Send[]) => Send[]> = this.filters$.pipe( + map( + (filters) => (sends: Send[]) => + sends.filter((send) => { + // do not show disabled sends + if (send.disabled) { + return false; + } + + if (filters.sendType !== null && send.type !== filters.sendType) { + return false; + } + + return true; + }), + ), + ); + + /** + * All available send types + */ + readonly sendTypes: ChipSelectOption[] = [ + { + value: SendType.File, + label: this.i18nService.t("file"), + icon: "bwi-globe", + }, + { + value: SendType.Text, + label: this.i18nService.t("text"), + icon: "bwi-credit-card", + }, + ]; + + /** Resets `filterForm` to the original state */ + resetFilterForm(): void { + this.filterForm.reset(INITIAL_FILTERS); + } + + /** + * Converts the given item into the `ChipSelectOption` structure + */ + private convertToChipSelectOption( + item: TreeNode, + icon: string, + ): ChipSelectOption { + return { + value: item.node, + label: item.node.name, + icon, + children: item.children + ? item.children.map((i) => this.convertToChipSelectOption(i, icon)) + : undefined, + }; + } +}