From 8995d09481c118a40a29108490191ae11532fee6 Mon Sep 17 00:00:00 2001 From: Tom Winter Date: Wed, 10 Jan 2024 18:49:28 +0100 Subject: [PATCH] feat(filter): enable multi-selection filter --- .../notes-manager/notes-manager.component.ts | 12 +- .../date-range-filter-panel.component.html | 2 +- .../date-range-filter-panel.component.spec.ts | 11 +- .../date-range-filter-panel.component.ts | 16 +- .../date-range-filter.component.spec.ts | 47 ++--- .../date-range-filter.component.ts | 17 +- .../default-interaction-types.ts | 4 - src/app/core/entity-list/EntityListConfig.ts | 4 +- src/app/core/entity/model/entity.ts | 2 +- .../filter-generator.service.spec.ts | 48 ++--- .../filter-generator.service.ts | 15 +- src/app/core/filter/filter.service.ts | 27 ++- .../core/filter/filter/filter.component.html | 2 +- .../filter/filter/filter.component.spec.ts | 16 +- .../core/filter/filter/filter.component.ts | 32 ++- src/app/core/filter/filters/booleanFilter.ts | 29 +++ .../filter/filters/configurableEnumFilter.ts | 25 +++ src/app/core/filter/filters/dateFilter.ts | 65 ++++++ src/app/core/filter/filters/entityFilter.ts | 22 ++ src/app/core/filter/filters/filters.spec.ts | 72 +++---- src/app/core/filter/filters/filters.ts | 188 ++---------------- .../list-filter/list-filter.component.html | 13 +- .../list-filter/list-filter.component.spec.ts | 2 +- .../list-filter/list-filter.component.ts | 23 +-- .../todos/todo-list/todo-list.component.ts | 3 +- tsconfig.json | 1 + 26 files changed, 366 insertions(+), 332 deletions(-) create mode 100644 src/app/core/filter/filters/booleanFilter.ts create mode 100644 src/app/core/filter/filters/configurableEnumFilter.ts create mode 100644 src/app/core/filter/filters/dateFilter.ts create mode 100644 src/app/core/filter/filters/entityFilter.ts diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts index 972167ac4f..cfd4932806 100644 --- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts +++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts @@ -22,6 +22,7 @@ import { Angulartics2Module } from "angulartics2"; import { MatMenuModule } from "@angular/material/menu"; import { FaDynamicIconComponent } from "../../../core/common-components/fa-dynamic-icon/fa-dynamic-icon.component"; import { RouteTarget } from "../../../route-target"; +import { DataFilter } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; /** * additional config specifically for NotesManagerComponent @@ -63,28 +64,27 @@ export class NotesManagerComponent implements OnInit { { key: "urgent", label: $localize`:Filter-option for notes:Urgent`, - filter: { "warningLevel.id": WarningLevel.URGENT }, + filter: { "warningLevel.id": WarningLevel.URGENT } as DataFilter, }, { key: "follow-up", label: $localize`:Filter-option for notes:Needs Follow-Up`, filter: { - "warningLevel.id": { $in: [WarningLevel.URGENT, WarningLevel.WARNING] }, - }, + "warningLevel.id": { $in: [WarningLevel.WARNING] }, + } as DataFilter, }, - { key: "", label: $localize`All`, filter: {} }, ]; private dateFS: FilterSelectionOption[] = [ { key: "current-week", label: $localize`:Filter-option for notes:This Week`, - filter: { date: this.getWeeksFilter(0) }, + filter: { date: this.getWeeksFilter(0) } as DataFilter, }, { key: "last-week", label: $localize`:Filter-option for notes:Since Last Week`, - filter: { date: this.getWeeksFilter(1) }, + filter: { date: this.getWeeksFilter(1) } as DataFilter, }, { key: "", label: $localize`All`, filter: {} }, ]; diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html index b0bf659070..14cbed0aba 100644 --- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html +++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html @@ -18,7 +18,7 @@ (mouseenter)="preselectAllRange()" (mouseleave)="unselectRange()" (click)="selectRangeAndClose('all')" - [class.selected-option]="filter.selectedOption === '_'" + [class.selected-option]="filter.selectedOptionsKeys.length === 0" > All diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts index b63dc2ae5f..414a10069a 100644 --- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts +++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts @@ -12,7 +12,8 @@ import { HarnessLoader } from "@angular/cdk/testing"; import { DateRange } from "@angular/material/datepicker"; import { MatCalendarHarness } from "@angular/material/datepicker/testing"; import moment from "moment"; -import { DateFilter } from "../../../../filter/filters/filters"; + +import { DateFilter } from "../../../../filter/filters/dateFilter"; describe("DateRangeFilterPanelComponent", () => { let component: DateRangeFilterPanelComponent; @@ -22,7 +23,7 @@ describe("DateRangeFilterPanelComponent", () => { beforeEach(async () => { dateFilter = new DateFilter("test", "Test", defaultDateFilters); - dateFilter.selectedOption = "1"; + dateFilter.selectedOptionsKeys = ["1"]; jasmine.clock().mockDate(moment("2023-04-08").toDate()); await TestBed.configureTestingModule({ imports: [MatNativeDateModule], @@ -85,7 +86,7 @@ describe("DateRangeFilterPanelComponent", () => { moment("2023-04-08").startOf("day").toDate(), ); expect(filterRange.end).toEqual(moment("2023-04-08").endOf("day").toDate()); - expect(dateFilter.selectedOption).toBe("0"); + expect(dateFilter.selectedOptionsKeys).toEqual(["0"]); }); it("should highlight the date range when hovering over a option", async () => { @@ -114,9 +115,9 @@ describe("DateRangeFilterPanelComponent", () => { } }); - it("should return '_' as filter.selectedOption when 'all' option has been chosen", async () => { + it("should return empty array as filter.selectedOption when 'all' option has been chosen", async () => { component.selectRangeAndClose("all"); - expect(dateFilter.selectedOption).toEqual("_"); + expect(dateFilter.selectedOptionsKeys).toEqual([]); }); it("should correctly calculate date ranges based on the config", () => { diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts index 1764e848b8..10cb51c6b4 100644 --- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts +++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts @@ -1,10 +1,10 @@ import { Component, Inject } from "@angular/core"; import { DateRange, + MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER, MatDatepickerModule, MatDateSelectionModel, MatRangeDateSelectionModel, - MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER, } from "@angular/material/datepicker"; import { MAT_DIALOG_DATA, @@ -16,8 +16,8 @@ import { NgForOf } from "@angular/common"; import { DateRangeFilterConfigOption } from "../../../../entity-list/EntityListConfig"; import moment from "moment"; import { FormsModule } from "@angular/forms"; -import { DateFilter } from "../../../../filter/filters/filters"; import { dateToString } from "../../../../../utils/utils"; +import { DateFilter } from "../../../../filter/filters/dateFilter"; export const defaultDateFilters: DateRangeFilterConfigOption[] = [ { @@ -91,9 +91,9 @@ export class DateRangeFilterPanelComponent { selectRangeAndClose(index: number | "all"): void { if (typeof index === "number") { - this.filter.selectedOption = index.toString(); + this.filter.selectedOptionsKeys = [index.toString()]; } else { - this.filter.selectedOption = "_"; + this.filter.selectedOptionsKeys = []; } this.dialogRef.close(); } @@ -102,11 +102,11 @@ export class DateRangeFilterPanelComponent { if (!this.selectedRangeValue?.start || this.selectedRangeValue?.end) { this.selectedRangeValue = new DateRange(selectedDate, null); } else { - const start = this.selectedRangeValue.start; - this.filter.selectedOption = + const start: Date = this.selectedRangeValue.start; + this.filter.selectedOptionsKeys = start < selectedDate - ? dateToString(start) + "_" + dateToString(selectedDate) - : dateToString(selectedDate) + "_" + dateToString(start); + ? [dateToString(start), dateToString(selectedDate)] + : [dateToString(selectedDate), dateToString(start)]; this.dialogRef.close(); } } diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts index 69b266e69c..9ffadea7ee 100644 --- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts +++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts @@ -4,9 +4,9 @@ import { DateRangeFilterComponent } from "./date-range-filter.component"; import { MatDialog } from "@angular/material/dialog"; import { MatNativeDateModule } from "@angular/material/core"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { DateFilter } from "../../../filter/filters/filters"; import { defaultDateFilters } from "./date-range-filter-panel/date-range-filter-panel.component"; import moment from "moment"; +import { DateFilter } from "../../../filter/filters/dateFilter"; describe("DateRangeFilterComponent", () => { let component: DateRangeFilterComponent; @@ -30,12 +30,12 @@ describe("DateRangeFilterComponent", () => { it("should set the correct date filter when a new option is selected", () => { const dateFilter = new DateFilter("test", "Test", defaultDateFilters); - dateFilter.selectedOption = "9"; + dateFilter.selectedOptionsKeys = ["9"]; component.filterConfig = dateFilter; - expect(component.dateFilter.getFilter()).toEqual({}); + expect(component.dateFilter.getFilters()).toEqual([{}]); jasmine.clock().mockDate(moment("2023-05-18").toDate()); - dateFilter.selectedOption = "0"; + dateFilter.selectedOptionsKeys = ["0"]; component.filterConfig = dateFilter; let expectedDataFilter = { test: { @@ -43,9 +43,9 @@ describe("DateRangeFilterComponent", () => { $lte: "2023-05-18", }, }; - expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); + expect(component.dateFilter.getFilters()).toEqual([expectedDataFilter]); - dateFilter.selectedOption = "1"; + dateFilter.selectedOptionsKeys = ["1"]; component.filterConfig = dateFilter; expectedDataFilter = { test: { @@ -53,42 +53,42 @@ describe("DateRangeFilterComponent", () => { $lte: "2023-05-20", }, }; - expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); + expect(component.dateFilter.getFilters()).toEqual([expectedDataFilter]); - dateFilter.selectedOption = "_"; + dateFilter.selectedOptionsKeys = []; component.filterConfig = dateFilter; - expect(component.dateFilter.getFilter()).toEqual({}); + expect(component.dateFilter.getFilters()).toEqual([{}]); jasmine.clock().uninstall(); }); it("should set the correct date filter when inputting a specific date range via the URL", () => { let dateFilter = new DateFilter("test", "test", []); - dateFilter.selectedOption = "1_2_3"; + dateFilter.selectedOptionsKeys = ["1_2_3"]; component.filterConfig = dateFilter; - expect(component.dateFilter.getFilter()).toEqual({}); + expect(component.dateFilter.getFilters()).toEqual([{}]); - dateFilter.selectedOption = "_"; + dateFilter.selectedOptionsKeys = []; component.filterConfig = dateFilter; - expect(component.dateFilter.getFilter()).toEqual({}); + expect(component.dateFilter.getFilters()).toEqual([{}]); - dateFilter.selectedOption = "2022-9-18_"; + dateFilter.selectedOptionsKeys = ["2022-9-18", ""]; component.filterConfig = dateFilter; let testFilter: { $gte?: string; $lte?: string } = { $gte: "2022-09-18" }; let expectedDateFilter = { test: testFilter, }; - expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter); + expect(component.dateFilter.getFilters()).toEqual([expectedDateFilter]); - dateFilter.selectedOption = "_2023-01-3"; + dateFilter.selectedOptionsKeys = ["", "2023-01-3"]; component.filterConfig = dateFilter; testFilter = { $lte: "2023-01-03" }; expectedDateFilter = { test: testFilter, }; - expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter); + expect(component.dateFilter.getFilters()).toEqual([expectedDateFilter]); - dateFilter.selectedOption = "2022-9-18_2023-01-3"; + dateFilter.selectedOptionsKeys = ["2022-9-18", "2023-01-3"]; component.filterConfig = dateFilter; testFilter = { $gte: "2022-09-18", @@ -97,7 +97,7 @@ describe("DateRangeFilterComponent", () => { expectedDateFilter = { test: testFilter, }; - expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter); + expect(component.dateFilter.getFilters()).toEqual([expectedDateFilter]); }); it("should set the correct date filter when changing the date range manually", () => { @@ -107,15 +107,16 @@ describe("DateRangeFilterComponent", () => { component.dateChangedManually(); - expect(component.dateFilter.selectedOption).toEqual( - "2021-10-28_2024-02-12", - ); + expect(component.dateFilter.selectedOptionsKeys).toEqual([ + "2021-10-28", + "2024-02-12", + ]); let expectedDataFilter = { test: { $gte: "2021-10-28", $lte: "2024-02-12", }, }; - expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); + expect(component.dateFilter.getFilters()).toEqual([expectedDataFilter]); }); }); diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts index be2a4a83ef..874fc63b8d 100644 --- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts +++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts @@ -1,12 +1,13 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { Entity } from "../../../entity/model/entity"; -import { DateFilter, Filter } from "../../../filter/filters/filters"; +import { Filter } from "../../../filter/filters/filters"; import { DateRangeFilterPanelComponent } from "./date-range-filter-panel/date-range-filter-panel.component"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatDatepickerModule } from "@angular/material/datepicker"; import { FormsModule } from "@angular/forms"; import { dateToString, isValidDate } from "../../../../utils/utils"; +import { DateFilter } from "../../../filter/filters/dateFilter"; @Component({ selector: "app-date-range-filter", @@ -20,7 +21,7 @@ export class DateRangeFilterComponent { toDate: Date; dateFilter: DateFilter; - @Output() selectedOptionChange = new EventEmitter(); + @Output() selectedOptionChange = new EventEmitter(); @Input() set filterConfig(value: Filter) { this.dateFilter = value as DateFilter; @@ -37,16 +38,16 @@ export class DateRangeFilterComponent { ) { this.fromDate = range.start; this.toDate = range.end; - this.selectedOptionChange.emit(this.dateFilter.selectedOption); + this.selectedOptionChange.emit(this.dateFilter.selectedOptionsKeys); } } dateChangedManually() { - this.dateFilter.selectedOption = - (isValidDate(this.fromDate) ? dateToString(this.fromDate) : "") + - "_" + - (isValidDate(this.toDate) ? dateToString(this.toDate) : ""); - this.selectedOptionChange.emit(this.dateFilter.selectedOption); + this.dateFilter.selectedOptionsKeys = [ + isValidDate(this.fromDate) ? dateToString(this.fromDate) : "", + isValidDate(this.toDate) ? dateToString(this.toDate) : "", + ]; + this.selectedOptionChange.emit(this.dateFilter.selectedOptionsKeys); } openDialog(e: Event) { diff --git a/src/app/core/config/default-config/default-interaction-types.ts b/src/app/core/config/default-config/default-interaction-types.ts index ad8ddfee3f..44bc8350d9 100644 --- a/src/app/core/config/default-config/default-interaction-types.ts +++ b/src/app/core/config/default-config/default-interaction-types.ts @@ -1,10 +1,6 @@ import { InteractionType } from "../../../child-dev-project/notes/model/interaction-type.interface"; export const defaultInteractionTypes: InteractionType[] = [ - { - id: "", - label: "", - }, { id: "VISIT", label: $localize`:Interaction type/Category of a Note:Home Visit`, diff --git a/src/app/core/entity-list/EntityListConfig.ts b/src/app/core/entity-list/EntityListConfig.ts index 0491f51340..8b6b6c65d4 100644 --- a/src/app/core/entity-list/EntityListConfig.ts +++ b/src/app/core/entity-list/EntityListConfig.ts @@ -23,7 +23,7 @@ export interface EntityListConfig { /** * Optional config for which columns are displayed. - * By default all columns are shown + * By default, all columns are shown */ columnGroups?: ColumnGroupsConfig; @@ -55,7 +55,7 @@ export interface ColumnGroupsConfig { default?: string; /** - * The name of the group group that should be selected by default on a mobile device. + * The name of the group that should be selected by default on a mobile device. * Default is the name of the first group. */ mobile?: string; diff --git a/src/app/core/entity/model/entity.ts b/src/app/core/entity/model/entity.ts index 1a05718d53..ae0ee720fd 100644 --- a/src/app/core/entity/model/entity.ts +++ b/src/app/core/entity/model/entity.ts @@ -66,7 +66,7 @@ export class Entity { /** * True if this type's schema has been customized dynamically from the config. */ - static _isCustomizedType?: boolean; + static _isCustomizedType?: boolean; // todo should be private or renamed to "isCustomizedType" /** * Defining which attribute values of an entity should be shown in the `.toString()` method. diff --git a/src/app/core/filter/filter-generator/filter-generator.service.spec.ts b/src/app/core/filter/filter-generator/filter-generator.service.spec.ts index e39895f970..75d2af2491 100644 --- a/src/app/core/filter/filter-generator/filter-generator.service.spec.ts +++ b/src/app/core/filter/filter-generator/filter-generator.service.spec.ts @@ -14,15 +14,12 @@ import { Child } from "../../../child-dev-project/children/model/child"; import moment from "moment"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { FilterService } from "../filter.service"; -import { - BooleanFilter, - ConfigurableEnumFilter, - DateFilter, - EntityFilter, - FilterSelectionOption, - SelectableFilter, -} from "../filters/filters"; +import { FilterSelectionOption, SelectableFilter } from "../filters/filters"; import { Entity } from "../../entity/model/entity"; +import { DateFilter } from "../filters/dateFilter"; +import { BooleanFilter } from "../filters/booleanFilter"; +import { ConfigurableEnumFilter } from "../filters/configurableEnumFilter"; +import { EntityFilter } from "../filters/entityFilter"; describe("FilterGeneratorService", () => { let service: FilterGeneratorService; @@ -61,7 +58,6 @@ describe("FilterGeneratorService", () => { return { key: option.key, label: option.label }; }), ).toEqual([ - { key: "all", label: "All" }, { key: "true", label: "Private" }, { key: "false", label: "Government" }, ]); @@ -71,9 +67,6 @@ describe("FilterGeneratorService", () => { const interactionTypes = defaultInteractionTypes.map((it) => jasmine.objectContaining({ key: it.id, label: it.label }), ); - interactionTypes.push( - jasmine.objectContaining({ key: "all", label: "All" }), - ); const schema = Note.schema.get("category"); let filterOptions = ( @@ -133,10 +126,9 @@ describe("FilterGeneratorService", () => { defaultInteractionTypes[2], ]; - // indices are increased by one as first option is "all" + expect(filter([note], filterOptions.options[1])).toEqual([note]); expect(filter([note], filterOptions.options[2])).toEqual([note]); - expect(filter([note], filterOptions.options[3])).toEqual([note]); - expect(filter([note], filterOptions.options[4])).toEqual([]); + expect(filter([note], filterOptions.options[3])).toEqual([]); Note.schema.delete("otherEnum"); }); @@ -164,17 +156,17 @@ describe("FilterGeneratorService", () => { expect(filterOptions.label).toEqual(schema.label); expect(filterOptions.name).toEqual("schoolId"); const allRelations = [csr1, csr2, csr3, csr4]; - const allFilter = filterOptions.options.find((opt) => opt.key === "all"); - expect(allFilter.label).toEqual("All"); - expect(filter(allRelations, allFilter)).toEqual(allRelations); - const school1Filter = filterOptions.options.find( - (opt) => opt.key === school1.getId(), - ); + // const allFilter: FilterSelectionOption = filterOptions.options.find( + // (opt) => opt.key === "all", + // ); + // expect(allFilter.label).toEqual("All"); + // expect(filter(allRelations, allFilter)).toEqual(allRelations); + const school1Filter: FilterSelectionOption = + filterOptions.options.find((opt) => opt.key === school1.getId()); expect(school1Filter.label).toEqual(school1.name); expect(filter(allRelations, school1Filter)).toEqual([csr1, csr4]); - const school2Filter = filterOptions.options.find( - (opt) => opt.key === school2.getId(), - ); + const school2Filter: FilterSelectionOption = + filterOptions.options.find((opt) => opt.key === school2.getId()); expect(school2Filter.label).toEqual(school2.name); expect(filter(allRelations, school2Filter)).toEqual([csr2, csr3]); }); @@ -203,7 +195,6 @@ describe("FilterGeneratorService", () => { }); expect(comparableOptions).toEqual( jasmine.arrayWithExactContents([ - { key: "", label: "All" }, { key: "muslim", label: "muslim" }, { key: "christian", label: "christian" }, ]), @@ -218,7 +209,6 @@ describe("FilterGeneratorService", () => { label: "Date", default: "today", options: [ - { key: "", label: "All", filter: {} }, { key: "today", label: "Today", @@ -239,15 +229,15 @@ describe("FilterGeneratorService", () => { expect(filterOptions.label).toEqual(prebuiltFilter.label); expect(filterOptions.name).toEqual(prebuiltFilter.id); expect(filterOptions.options).toEqual(prebuiltFilter.options); - expect(filterOptions.selectedOption).toEqual(prebuiltFilter.default); + expect(filterOptions.selectedOptionsKeys).toEqual([prebuiltFilter.default]); const todayNote = new Note(); todayNote.date = new Date(); const yesterdayNote = new Note(); const notes = [todayNote, yesterdayNote]; yesterdayNote.date = moment().subtract(1, "day").toDate(); - const allFilter = filterOptions.options.find((f) => f.key === ""); - expect(filter(notes, allFilter)).toEqual(notes); + // const allFilter = filterOptions.options.find((f) => f.key === ""); + // expect(filter(notes, allFilter)).toEqual(notes); const todayFilter = filterOptions.options.find((f) => f.key === "today"); expect(filter(notes, todayFilter)).toEqual([todayNote]); const beforeFilter = filterOptions.options.find((f) => f.key === "before"); diff --git a/src/app/core/filter/filter-generator/filter-generator.service.ts b/src/app/core/filter/filter-generator/filter-generator.service.ts index a07e83e514..962bbc74a5 100644 --- a/src/app/core/filter/filter-generator/filter-generator.service.ts +++ b/src/app/core/filter/filter-generator/filter-generator.service.ts @@ -1,10 +1,7 @@ import { Injectable } from "@angular/core"; import { - BooleanFilter, - ConfigurableEnumFilter, - DateFilter, - EntityFilter, Filter, + FilterSelectionOption, SelectableFilter, } from "../filters/filters"; import { @@ -21,6 +18,10 @@ import { FilterService } from "../filter.service"; import { defaultDateFilters } from "../../basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component"; import { EntitySchemaService } from "../../entity/schema/entity-schema.service"; import { DateDatatype } from "../../basic-datatypes/date/date.datatype"; +import { DateFilter } from "../filters/dateFilter"; +import { BooleanFilter } from "../filters/booleanFilter"; +import { ConfigurableEnumFilter } from "../filters/configurableEnumFilter"; +import { EntityFilter } from "../filters/entityFilter"; @Injectable({ providedIn: "root", @@ -99,7 +100,9 @@ export class FilterGeneratorService { ); } else { const options = [...new Set(data.map((c) => c[filterConfig.id]))]; - const fSO = SelectableFilter.generateOptions(options, filterConfig.id); + const fSO: FilterSelectionOption[] = + SelectableFilter.generateOptions(options, filterConfig.id); + filter = new SelectableFilter( filterConfig.id, fSO, @@ -108,7 +111,7 @@ export class FilterGeneratorService { } if (filterConfig.hasOwnProperty("default")) { - filter.selectedOption = filterConfig.default; + filter.selectedOptionsKeys = [filterConfig.default]; } if (filter instanceof SelectableFilter) { diff --git a/src/app/core/filter/filter.service.ts b/src/app/core/filter/filter.service.ts index 178652f229..a4756b890d 100644 --- a/src/app/core/filter/filter.service.ts +++ b/src/app/core/filter/filter.service.ts @@ -11,6 +11,7 @@ import { } from "@ucast/mongo2js"; import moment from "moment"; import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service"; +import { Filter as EntityFilter } from "./filters/filters"; /** * Utility service to help handling and aligning filters with entities. @@ -27,6 +28,29 @@ export class FilterService { constructor(private enumService: ConfigurableEnumService) {} + combineFilters( + entityFilters: EntityFilter[], + ): DataFilter { + console.log("entityFilters", entityFilters); + if (entityFilters.length === 0) { + return {} as DataFilter; + } + + return { + $and: [ + ...entityFilters.map((value: EntityFilter): DataFilter => { + let filters: DataFilter[] = value.getFilters(); + if (filters.length === 0) { + return {} as DataFilter; + } + return { + $or: [...value.getFilters()], + } as unknown as DataFilter; + }), + ], + } as unknown as DataFilter; + } + /** * Builds a predicate for a given filter object. * This predicate can be used to filter arrays of objects. @@ -36,6 +60,7 @@ export class FilterService { * ``` * @param filter a valid filter object, e.g. as provided by the `FilterComponent` */ + // todo: check usage for array usage (typing not working) getFilterPredicate(filter: DataFilter) { return this.filterFactory(filter); } @@ -44,7 +69,7 @@ export class FilterService { * Patches an entity with values required to pass the filter query. * This patch happens in-place. * @param entity the entity to be patched - * @param filter the filter which the entity should pass afterwards + * @param filter the filter which the entity should pass afterward */ alignEntityWithFilter(entity: T, filter: DataFilter) { const schema = entity.getSchema(); diff --git a/src/app/core/filter/filter/filter.component.html b/src/app/core/filter/filter/filter.component.html index 50977eb8c8..4c63f19492 100644 --- a/src/app/core/filter/filter/filter.component.html +++ b/src/app/core/filter/filter/filter.component.html @@ -2,7 +2,7 @@ { it("should set up category filter from configurable enum", async () => { component.entityType = Note; - const t1 = defaultInteractionTypes[1]; + const t1 = defaultInteractionTypes[0]; const n1 = new Note(); n1.category = t1; const n2 = new Note(); - n2.category = defaultInteractionTypes[2]; + n2.category = defaultInteractionTypes[1]; component.entities = [n1, n2]; component.onlyShowRelevantFilterOptions = true; component.filterConfig = [{ id: "category" }]; @@ -44,14 +44,20 @@ describe("FilterComponent", () => { const selection = await loader.getHarness(MatSelectHarness); await selection.open(); const options = await selection.getOptions(); - expect(options).toHaveSize(3); + expect(options).toHaveSize(2); - const selectedOption = await options[1].getText(); + const selectedOption = await options[0].getText(); expect(selectedOption).toEqual(t1.label); await options[1].click(); const selected = await selection.getValueText(); expect(selected).toEqual(t1.label); - expect(component.filterObj).toEqual({ "category.id": t1.id } as any); + expect(component.filterObj).toEqual({ + $and: [ + { + $or: [{ "category.id": t1.id }], + }, + ], + } as any); }); }); diff --git a/src/app/core/filter/filter/filter.component.ts b/src/app/core/filter/filter/filter.component.ts index 710298369f..0cdf658200 100644 --- a/src/app/core/filter/filter/filter.component.ts +++ b/src/app/core/filter/filter/filter.component.ts @@ -11,12 +11,13 @@ import { Entity, EntityConstructor } from "../../entity/model/entity"; import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; import { FilterGeneratorService } from "../filter-generator/filter-generator.service"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { getUrlWithoutParams } from "../../../utils/utils"; import { ListFilterComponent } from "../list-filter/list-filter.component"; -import { NgForOf, NgIf } from "@angular/common"; +import { AsyncPipe, JsonPipe, NgForOf, NgIf } from "@angular/common"; import { Angulartics2Module } from "angulartics2"; import { DateRangeFilterComponent } from "../../basic-datatypes/date/date-range-filter/date-range-filter.component"; import { Filter } from "../filters/filters"; +import { getUrlWithoutParams } from "../../../utils/utils"; +import { FilterService } from "../filter.service"; /** * This component can be used to display filters, for example above tables. @@ -30,6 +31,8 @@ import { Filter } from "../filters/filters"; Angulartics2Module, DateRangeFilterComponent, NgIf, + JsonPipe, + AsyncPipe, ], standalone: true, }) @@ -63,7 +66,7 @@ export class FilterComponent implements OnChanges { */ @Input() filterObj: DataFilter; /** - * A event emitter that notifies about updates of the filter. + * An event emitter that notifies about updates of the filter. */ @Output() filterObjChange = new EventEmitter>(); @@ -72,6 +75,7 @@ export class FilterComponent implements OnChanges { constructor( private filterGenerator: FilterGeneratorService, + private filterService: FilterService, private router: Router, private route: ActivatedRoute, ) {} @@ -89,19 +93,19 @@ export class FilterComponent implements OnChanges { } } - filterOptionSelected(filter: Filter, selectedOption: string) { - filter.selectedOption = selectedOption; + filterOptionSelected(filter: Filter, selectedOptions: string[]) { + filter.selectedOptionsKeys = selectedOptions; this.applyFilterSelections(); if (this.useUrlQueryParams) { - this.updateUrl(filter.name, selectedOption); + this.updateUrl(filter.name, selectedOptions.toString()); } } private applyFilterSelections() { - const previousFilter = JSON.stringify(this.filterObj); - const newFilter = this.filterSelections.reduce( - (obj, filter) => Object.assign(obj, filter.getFilter()), - {} as DataFilter, + const previousFilter: string = JSON.stringify(this.filterObj); + + const newFilter: DataFilter = this.filterService.combineFilters( + this.filterSelections, ); if (previousFilter === JSON.stringify(newFilter)) { @@ -129,7 +133,13 @@ export class FilterComponent implements OnChanges { const params = parameters || this.route.snapshot.queryParams; this.filterSelections.forEach((f) => { if (params.hasOwnProperty(f.name)) { - f.selectedOption = params[f.name]; + if (params[f.name].includes(",")) { + f.selectedOptionsKeys = params[f.name].split(","); + } else { + f.selectedOptionsKeys = [params[f.name]]; + } + } else { + f.selectedOptionsKeys = []; } }); } diff --git a/src/app/core/filter/filters/booleanFilter.ts b/src/app/core/filter/filters/booleanFilter.ts new file mode 100644 index 0000000000..f060ed20da --- /dev/null +++ b/src/app/core/filter/filters/booleanFilter.ts @@ -0,0 +1,29 @@ +import { Entity } from "../../entity/model/entity"; +import { BooleanFilterConfig } from "../../entity-list/EntityListConfig"; +import { SelectableFilter } from "./filters"; +import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; + +export class BooleanFilter extends SelectableFilter { + constructor(name: string, label: string, config?: BooleanFilterConfig) { + super( + name, + [ + { + key: "true", + label: + config.true ?? $localize`:Filter label default boolean true:Yes`, + filter: { [config.id]: true } as DataFilter, + }, + { + key: "false", + label: + config.false ?? $localize`:Filter label default boolean true:No`, + filter: { + [config.id]: { $in: [false, undefined] }, + } as DataFilter, + }, + ], + label, + ); + } +} diff --git a/src/app/core/filter/filters/configurableEnumFilter.ts b/src/app/core/filter/filters/configurableEnumFilter.ts new file mode 100644 index 0000000000..c23b6f5fcb --- /dev/null +++ b/src/app/core/filter/filters/configurableEnumFilter.ts @@ -0,0 +1,25 @@ +import { Entity } from "../../entity/model/entity"; +import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface"; +import { FilterSelectionOption, SelectableFilter } from "./filters"; +import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; + +export class ConfigurableEnumFilter< + T extends Entity, +> extends SelectableFilter { + constructor( + name: string, + label: string, + enumValues: ConfigurableEnumValue[], + ) { + let options: FilterSelectionOption[] = []; + options.push( + ...enumValues.map((enumValue: ConfigurableEnumValue) => ({ + key: enumValue.id, + label: enumValue.label, + color: enumValue.color, + filter: { [name + ".id"]: enumValue.id } as DataFilter, + })), + ); + super(name, options, label); + } +} diff --git a/src/app/core/filter/filters/dateFilter.ts b/src/app/core/filter/filters/dateFilter.ts new file mode 100644 index 0000000000..5ef041a603 --- /dev/null +++ b/src/app/core/filter/filters/dateFilter.ts @@ -0,0 +1,65 @@ +import { Entity } from "../../entity/model/entity"; +import { DateRangeFilterConfigOption } from "../../entity-list/EntityListConfig"; +import { DateRange } from "@angular/material/datepicker"; +import { calculateDateRange } from "../../basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component"; +import moment from "moment"; +import { isValidDate } from "../../../utils/utils"; +import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; +import { Filter } from "./filters"; + +/** + * Represents a filter for date values. + * The filter can either be one of the predefined options or two manually entered dates. + */ +export class DateFilter extends Filter { + constructor( + public name: string, + public label: string = name, + public rangeOptions: DateRangeFilterConfigOption[], + ) { + super(name, label); + this.selectedOptionsKeys = []; + } + + /** + * Returns the date range according to the selected option or dates + */ + getDateRange(): DateRange { + if (this.getSelectedOption()) { + return calculateDateRange(this.getSelectedOption()); + } + const dates = this.selectedOptionsKeys; + if (dates?.length == 2) { + const firstDate = moment(dates[0]).toDate(); + const secondDate = moment(dates[1]).toDate(); + return new DateRange( + isValidDate(firstDate) ? firstDate : undefined, + isValidDate(secondDate) ? secondDate : undefined, + ); + } + return new DateRange(undefined, undefined); + } + + getFilters(): DataFilter[] { + const range = this.getDateRange(); + const filterObject: { $gte?: string; $lte?: string } = {}; + if (range.start) { + filterObject.$gte = moment(range.start).format("YYYY-MM-DD"); + } + if (range.end) { + filterObject.$lte = moment(range.end).format("YYYY-MM-DD"); + } + if (filterObject.$gte || filterObject.$lte) { + return [ + { + [this.name]: filterObject, + } as DataFilter, + ]; + } + return [{} as DataFilter]; + } + + getSelectedOption() { + return this.rangeOptions[this.selectedOptionsKeys as any]; + } +} diff --git a/src/app/core/filter/filters/entityFilter.ts b/src/app/core/filter/filters/entityFilter.ts new file mode 100644 index 0000000000..d26af48246 --- /dev/null +++ b/src/app/core/filter/filters/entityFilter.ts @@ -0,0 +1,22 @@ +import { Entity } from "../../entity/model/entity"; +import { FilterSelectionOption, SelectableFilter } from "./filters"; + +export class EntityFilter extends SelectableFilter { + constructor(name: string, label: string, filterEntities) { + filterEntities.sort((a, b) => a.toString().localeCompare(b.toString())); + const options: FilterSelectionOption[] = []; + options.push( + ...filterEntities.map((filterEntity) => ({ + key: filterEntity.getId(), + label: filterEntity.toString(), + filter: { + $or: [ + { [name]: filterEntity.getId() }, + { [name]: { $elemMatch: { $eq: filterEntity.getId() } } }, + ], + }, + })), + ); + super(name, options, label); + } +} diff --git a/src/app/core/filter/filters/filters.spec.ts b/src/app/core/filter/filters/filters.spec.ts index 4c85f0243f..a9c4f7d5cd 100644 --- a/src/app/core/filter/filters/filters.spec.ts +++ b/src/app/core/filter/filters/filters.spec.ts @@ -1,18 +1,32 @@ -import { BooleanFilter, Filter, SelectableFilter } from "./filters"; +import { Filter, SelectableFilter } from "./filters"; import { FilterService } from "../filter.service"; +import { BooleanFilter } from "./booleanFilter"; +import { Entity } from "../../entity/model/entity"; describe("Filters", () => { const filterService = new FilterService(undefined); function testFilter( - filterObj: Filter, + filterObj: Filter, testData: any[], expectedFilteredResult: any[], ) { - const filterPred = filterService.getFilterPredicate(filterObj.getFilter()); - const filtered = testData.filter(filterPred); - expect(filtered).toEqual(expectedFilteredResult); - return filtered; + const predicates = filterObj + .getFilters() + .map((df) => filterService.getFilterPredicate(df)); + + if (predicates.length == 0) { + return testData; + } + + let result = []; + + for (let i = 0; i < predicates.length; i++) { + result.push(...testData.filter(predicates[i])); + } + + expect(result).toEqual(expectedFilteredResult); + return result; } it("create an instance", () => { @@ -25,24 +39,24 @@ describe("Filters", () => { }); it("init new options", () => { - const fs = new SelectableFilter( - "", - [{ key: "", label: "", filter: "" }], - "", + const filter = new SelectableFilter( + "name", + [{ key: "option-1", label: "op", filter: {} }], + "name", ); - const keys = ["x", "y"]; - fs.options = SelectableFilter.generateOptions(keys, "category"); + const keys: string[] = ["x", "y"]; + filter.options = SelectableFilter.generateOptions(keys, "category"); - expect(fs.options).toHaveSize(keys.length + 1); + expect(filter.options).toHaveSize(keys.length); - fs.selectedOption = "x"; + filter.selectedOptionsKeys = ["x"]; const testData = [ { id: 1, category: "x" }, { id: 2, category: "y" }, ]; - const filteredData = testFilter(fs, testData, [testData[0]]); + const filteredData = testFilter(filter, testData, [testData[0]]); expect(filteredData[0].category).toBe("x"); }); @@ -58,27 +72,17 @@ describe("Filters", () => { const recordTrue = { value: true }; const recordFalse = { value: false }; - const recordUndefined = {}; - filter.selectedOption = "true"; - testFilter( - filter, - [recordFalse, recordTrue, recordUndefined], - [recordTrue], - ); + filter.selectedOptionsKeys = ["true"]; + testFilter(filter, [recordFalse, recordTrue], [recordTrue]); - filter.selectedOption = "false"; - testFilter( - filter, - [recordFalse, recordTrue, recordUndefined], - [recordFalse, recordUndefined], - ); + filter.selectedOptionsKeys = ["false"]; + testFilter(filter, [recordFalse, recordTrue], [recordFalse]); - filter.selectedOption = "all"; - testFilter( - filter, - [recordFalse, recordTrue, recordUndefined], - [recordFalse, recordTrue, recordUndefined], - ); + filter.selectedOptionsKeys = []; + testFilter(filter, [recordFalse, recordTrue], [recordFalse, recordTrue]); + + filter.selectedOptionsKeys = ["true", "false"]; + testFilter(filter, [recordFalse, recordTrue], [recordTrue, recordFalse]); }); }); diff --git a/src/app/core/filter/filters/filters.ts b/src/app/core/filter/filters/filters.ts index ab60a04953..f5a05f8cd0 100644 --- a/src/app/core/filter/filters/filters.ts +++ b/src/app/core/filter/filters/filters.ts @@ -15,82 +15,18 @@ * along with ndb-core. If not, see . */ -import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface"; -import { - BooleanFilterConfig, - DateRangeFilterConfigOption, -} from "../../entity-list/EntityListConfig"; import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; import { Entity } from "../../entity/model/entity"; -import { DateRange } from "@angular/material/datepicker"; -import { isValidDate } from "../../../utils/utils"; -import { calculateDateRange } from "../../basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component"; -import moment from "moment/moment"; export abstract class Filter { - public selectedOption: string; + public selectedOptionsKeys: string[] = []; - constructor( + protected constructor( public name: string, public label: string = name, ) {} - abstract getFilter(): DataFilter; -} - -/** - * Represents a filter for date values. - * The filter can either be one of the predefined options or two manually entered dates. - */ -export class DateFilter extends Filter { - constructor( - public name: string, - public label: string = name, - public rangeOptions: DateRangeFilterConfigOption[], - ) { - super(name, label); - this.selectedOption = "1"; - } - - /** - * Returns the date range according to the selected option or dates - */ - getDateRange(): DateRange { - if (this.getSelectedOption()) { - return calculateDateRange(this.getSelectedOption()); - } - const dates = this.selectedOption?.split("_"); - if (dates?.length == 2) { - const firstDate = moment(dates[0]).toDate(); - const secondDate = moment(dates[1]).toDate(); - return new DateRange( - isValidDate(firstDate) ? firstDate : undefined, - isValidDate(secondDate) ? secondDate : undefined, - ); - } - return new DateRange(undefined, undefined); - } - - getFilter(): DataFilter { - const range = this.getDateRange(); - const filterObject: { $gte?: string; $lte?: string } = {}; - if (range.start) { - filterObject.$gte = moment(range.start).format("YYYY-MM-DD"); - } - if (range.end) { - filterObject.$lte = moment(range.end).format("YYYY-MM-DD"); - } - if (filterObject.$gte || filterObject.$lte) { - return { - [this.name]: filterObject, - } as DataFilter; - } - return {} as DataFilter; - } - - getSelectedOption() { - return this.rangeOptions[this.selectedOption as any]; - } + abstract getFilters(): DataFilter[]; } /** @@ -119,13 +55,7 @@ export class SelectableFilter extends Filter { valuesToMatchAsOptions: string[], attributeName: string, ): FilterSelectionOption[] { - const options = [ - { - key: "", - label: $localize`:generic filter option showing all entries:All`, - filter: {} as DataFilter, - }, - ]; + const options: FilterSelectionOption[] = []; options.push( ...valuesToMatchAsOptions @@ -153,113 +83,37 @@ export class SelectableFilter extends Filter { public label: string = name, ) { super(name, label); - this.selectedOption = this.options[0]?.key; + this.selectedOptionsKeys = this.options.map( + (value: FilterSelectionOption) => value.key, + ); } - /** default filter will keep all items in the result */ - defaultFilter = {}; - /** * Get the full filter option by its key. * @param key The identifier of the requested option */ - getOption(key: string): FilterSelectionOption { - return this.options.find((option) => option.key === key); + getOption(key: string): FilterSelectionOption | undefined { + return this.options.find((option: FilterSelectionOption): boolean => { + return option.key === key; + }); } /** * Get the filter query for the given option. * If the given key is undefined or invalid, the returned filter matches any elements. */ - public getFilter(): DataFilter { - const option = this.getOption(this.selectedOption); - - if (!option) { - return this.defaultFilter as DataFilter; - } else { - return option.filter; - } - } -} - -export class BooleanFilter extends SelectableFilter { - constructor(name: string, label: string, config?: BooleanFilterConfig) { - super( - name, - [ - { - key: "all", - label: config.all ?? $localize`:Filter label:All`, - filter: {}, - }, - { - key: "true", - label: - config.true ?? $localize`:Filter label default boolean true:Yes`, - filter: { [config.id]: true }, - }, - { - key: "false", - label: - config.false ?? $localize`:Filter label default boolean true:No`, - filter: { $or: [{ [config.id]: false }, { [config.id]: undefined }] }, - }, - ], - label, - ); - } -} - -export class ConfigurableEnumFilter< - T extends Entity, -> extends SelectableFilter { - constructor( - name: string, - label: string, - enumValues: ConfigurableEnumValue[], - ) { - let options: FilterSelectionOption[] = [ - { - key: "all", - label: $localize`:Filter label:All`, - filter: {}, + public getFilters(): DataFilter[] { + const options: FilterSelectionOption[] = this.selectedOptionsKeys + .map((value: string) => this.getOption(value)) + .filter((value) => value !== undefined); + + let filters: DataFilter[] = options.map( + (previousValue: FilterSelectionOption) => { + return previousValue.filter as DataFilter; }, - ]; - options.push( - ...enumValues.map((enumValue) => ({ - key: enumValue.id, - label: enumValue.label, - color: enumValue.color, - filter: { [name + ".id"]: enumValue.id }, - })), ); - super(name, options, label); - } -} -export class EntityFilter extends SelectableFilter { - constructor(name: string, label: string, filterEntities) { - filterEntities.sort((a, b) => a.toString().localeCompare(b.toString())); - const options: FilterSelectionOption[] = [ - { - key: "all", - label: $localize`:Filter label:All`, - filter: {}, - }, - ]; - options.push( - ...filterEntities.map((filterEntity) => ({ - key: filterEntity.getId(), - label: filterEntity.toString(), - filter: { - $or: [ - { [name]: filterEntity.getId() }, - { [name]: { $elemMatch: { $eq: filterEntity.getId() } } }, - ], - }, - })), - ); - super(name, options, label); + return filters; } } @@ -280,5 +134,5 @@ export interface FilterSelectionOption { /** * The filter query which should be used if this filter is selected */ - filter: DataFilter | any; + filter: DataFilter; } diff --git a/src/app/core/filter/list-filter/list-filter.component.html b/src/app/core/filter/list-filter/list-filter.component.html index 523a6f4081..b92ed24291 100644 --- a/src/app/core/filter/list-filter/list-filter.component.html +++ b/src/app/core/filter/list-filter/list-filter.component.html @@ -1,14 +1,19 @@ - {{ _filterConfig.label || _filterConfig.name }} - + {{ filterConfig.label || filterConfig.name }} + + @for (option of filterConfig.options; track option.key) { {{ option.label }} + } diff --git a/src/app/core/filter/list-filter/list-filter.component.spec.ts b/src/app/core/filter/list-filter/list-filter.component.spec.ts index 701cbe4f11..a37e4a5f3b 100644 --- a/src/app/core/filter/list-filter/list-filter.component.spec.ts +++ b/src/app/core/filter/list-filter/list-filter.component.spec.ts @@ -1,8 +1,8 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { ListFilterComponent } from "./list-filter.component"; -import { SelectableFilter } from "../filters/filters"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; +import { SelectableFilter } from "../filters/filters"; describe("ListFilterComponent", () => { let component: ListFilterComponent; diff --git a/src/app/core/filter/list-filter/list-filter.component.ts b/src/app/core/filter/list-filter/list-filter.component.ts index 1c4093041e..2e561dcb6e 100644 --- a/src/app/core/filter/list-filter/list-filter.component.ts +++ b/src/app/core/filter/list-filter/list-filter.component.ts @@ -1,10 +1,11 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { Filter, SelectableFilter } from "../filters/filters"; import { Entity } from "../../entity/model/entity"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatSelectModule } from "@angular/material/select"; import { BorderHighlightDirective } from "../../common-components/border-highlight/border-highlight.directive"; -import { NgForOf } from "@angular/common"; +import { JsonPipe, NgForOf } from "@angular/common"; +import { ReactiveFormsModule } from "@angular/forms"; +import { SelectableFilter } from "../filters/filters"; @Component({ selector: "app-list-filter", @@ -12,22 +13,16 @@ import { NgForOf } from "@angular/common"; imports: [ MatFormFieldModule, MatSelectModule, + ReactiveFormsModule, BorderHighlightDirective, NgForOf, + JsonPipe, ], standalone: true, }) export class ListFilterComponent { - @Input() - public set filterConfig(value: Filter) { - this._filterConfig = value as SelectableFilter; - } - _filterConfig: SelectableFilter; - @Input() selectedOption: string; - @Output() selectedOptionChange = new EventEmitter(); - - selectOption(selectedOptionKey: string) { - this.selectedOption = selectedOptionKey; - this.selectedOptionChange.emit(selectedOptionKey); - } + @Input({ transform: (value: any) => value as SelectableFilter }) + filterConfig: SelectableFilter; + @Input() selectedOptions: string[]; + @Output() selectedOptionChange: EventEmitter = new EventEmitter(); } diff --git a/src/app/features/todos/todo-list/todo-list.component.ts b/src/app/features/todos/todo-list/todo-list.component.ts index 09bcdaef42..8675c2f3ab 100644 --- a/src/app/features/todos/todo-list/todo-list.component.ts +++ b/src/app/features/todos/todo-list/todo-list.component.ts @@ -15,6 +15,7 @@ import { FilterSelectionOption } from "../../../core/filter/filters/filters"; import { RouteTarget } from "../../../route-target"; import { CurrentUserSubject } from "../../../core/session/current-user-subject"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { DataFilter } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; @UntilDestroy() @RouteTarget("TodoList") @@ -160,5 +161,5 @@ const filterCurrentlyActive: FilterSelectionOption = { ], }, ], - }, + } as DataFilter, }; diff --git a/tsconfig.json b/tsconfig.json index d68d02204f..81a93e1f15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "types": ["jasmine"], "lib": [ "es2018", + "es2019", "dom" ], "paths": {