diff --git a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts index 7d95e56520..e080a43b13 100644 --- a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts +++ b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts @@ -51,7 +51,6 @@ describe("ChildrenListComponent", () => { default: "true", true: "Currently active children", false: "Currently inactive children", - all: "All children", } as BooleanFilterConfig, { id: "center", 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 21059bc7f6..aebb7b3ec0 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 @@ -3,7 +3,10 @@ import { Note } from "../model/note"; import { NoteDetailsComponent } from "../note-details/note-details.component"; import { ActivatedRoute } from "@angular/router"; import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; -import { FilterSelectionOption } from "../../../core/filter/filters/filters"; +import { + DataFilter, + FilterSelectionOption, +} from "../../../core/filter/filters/filters"; import { FormDialogService } from "../../../core/form-dialog/form-dialog.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { LoggingService } from "../../../core/logging/logging.service"; @@ -11,7 +14,6 @@ import { EntityListComponent } from "../../../core/entity-list/entity-list/entit import { applyUpdate } from "../../../core/entity/model/entity-update"; import { EntityListConfig } from "../../../core/entity-list/EntityListConfig"; import { EventNote } from "../../attendance/model/event-note"; -import { WarningLevel } from "../../warning-level"; import { DynamicComponentConfig } from "../../../core/config/dynamic-components/dynamic-component-config.interface"; import { merge } from "rxjs"; import moment from "moment"; @@ -58,32 +60,16 @@ export class NotesManagerComponent implements OnInit { entityConstructor = Note; notes: Note[]; - private statusFS: FilterSelectionOption[] = [ - { - key: "urgent", - label: $localize`:Filter-option for notes:Urgent`, - filter: { "warningLevel.id": WarningLevel.URGENT }, - }, - { - key: "follow-up", - label: $localize`:Filter-option for notes:Needs Follow-Up`, - filter: { - "warningLevel.id": { $in: [WarningLevel.URGENT, WarningLevel.WARNING] }, - }, - }, - { 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: {} }, ]; @@ -147,11 +133,6 @@ export class NotesManagerComponent implements OnInit { (filter) => filter.type === "prebuilt", )) { switch (prebuiltFilter.id) { - case "status": { - prebuiltFilter["options"] = this.statusFS; - prebuiltFilter["default"] = ""; - break; - } case "date": { prebuiltFilter["options"] = this.dateFS; prebuiltFilter["default"] = "current-week"; diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts index 169ff72cbb..e78205f0f7 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts @@ -30,10 +30,11 @@ export class ConfigurableEnumService { getEnumValues( id: string, ): T[] { - return this.getEnum(id).values as T[]; + const configurableEnum = this.getEnum(id); + return configurableEnum ? (configurableEnum.values as T[]) : []; } - getEnum(id: string): ConfigurableEnum { + getEnum(id: string): ConfigurableEnum | undefined { if (!this.enums) { return; } diff --git a/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts index 0a71e6821d..cdf6b647d9 100644 --- a/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts +++ b/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts @@ -63,7 +63,7 @@ export class EnumDropdownComponent implements OnChanges { if (changes.hasOwnProperty("enumId") || changes.hasOwnProperty("form")) { this.invalidOptions = this.prepareInvalidOptions(); } - this.options = [...this.enumEntity.values, ...this.invalidOptions]; + this.options = [...this.enumEntity?.values, ...this.invalidOptions]; } private prepareInvalidOptions(): ConfigurableEnumValue[] { 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..4e6944dbdb 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.selectedOptionValues.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..8acf5cb210 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.selectedOptionValues = ["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.selectedOptionValues).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.selectedOptionValues).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..d4871f900c 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.selectedOptionValues = [index.toString()]; } else { - this.filter.selectedOption = "_"; + this.filter.selectedOptionValues = []; } 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.selectedOptionValues = 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..52ffd25b2a 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.selectedOptionValues = ["9"]; component.filterConfig = dateFilter; expect(component.dateFilter.getFilter()).toEqual({}); jasmine.clock().mockDate(moment("2023-05-18").toDate()); - dateFilter.selectedOption = "0"; + dateFilter.selectedOptionValues = ["0"]; component.filterConfig = dateFilter; let expectedDataFilter = { test: { @@ -45,7 +45,7 @@ describe("DateRangeFilterComponent", () => { }; expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); - dateFilter.selectedOption = "1"; + dateFilter.selectedOptionValues = ["1"]; component.filterConfig = dateFilter; expectedDataFilter = { test: { @@ -55,7 +55,7 @@ describe("DateRangeFilterComponent", () => { }; expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); - dateFilter.selectedOption = "_"; + dateFilter.selectedOptionValues = []; component.filterConfig = dateFilter; expect(component.dateFilter.getFilter()).toEqual({}); jasmine.clock().uninstall(); @@ -64,15 +64,15 @@ describe("DateRangeFilterComponent", () => { 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.selectedOptionValues = ["1", "2", "3"]; component.filterConfig = dateFilter; expect(component.dateFilter.getFilter()).toEqual({}); - dateFilter.selectedOption = "_"; + dateFilter.selectedOptionValues = []; component.filterConfig = dateFilter; expect(component.dateFilter.getFilter()).toEqual({}); - dateFilter.selectedOption = "2022-9-18_"; + dateFilter.selectedOptionValues = ["2022-9-18", ""]; component.filterConfig = dateFilter; let testFilter: { $gte?: string; $lte?: string } = { $gte: "2022-09-18" }; let expectedDateFilter = { @@ -80,7 +80,7 @@ describe("DateRangeFilterComponent", () => { }; expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter); - dateFilter.selectedOption = "_2023-01-3"; + dateFilter.selectedOptionValues = ["", "2023-01-3"]; component.filterConfig = dateFilter; testFilter = { $lte: "2023-01-03" }; expectedDateFilter = { @@ -88,7 +88,7 @@ describe("DateRangeFilterComponent", () => { }; expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter); - dateFilter.selectedOption = "2022-9-18_2023-01-3"; + dateFilter.selectedOptionValues = ["2022-9-18", "2023-01-3"]; component.filterConfig = dateFilter; testFilter = { $gte: "2022-09-18", @@ -107,9 +107,10 @@ describe("DateRangeFilterComponent", () => { component.dateChangedManually(); - expect(component.dateFilter.selectedOption).toEqual( - "2021-10-28_2024-02-12", - ); + expect(component.dateFilter.selectedOptionValues).toEqual([ + "2021-10-28", + "2024-02-12", + ]); let expectedDataFilter = { test: { $gte: "2021-10-28", 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..301a1832ff 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.selectedOptionValues); } } dateChangedManually() { - this.dateFilter.selectedOption = - (isValidDate(this.fromDate) ? dateToString(this.fromDate) : "") + - "_" + - (isValidDate(this.toDate) ? dateToString(this.toDate) : ""); - this.selectedOptionChange.emit(this.dateFilter.selectedOption); + this.dateFilter.selectedOptionValues = [ + isValidDate(this.fromDate) ? dateToString(this.fromDate) : "", + isValidDate(this.toDate) ? dateToString(this.toDate) : "", + ]; + this.selectedOptionChange.emit(this.dateFilter.selectedOptionValues); } openDialog(e: Event) { diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 6f25242354..43439959a6 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -194,9 +194,7 @@ export const defaultJsonConfig = { }, "filters": [ { - "id": "status", - "label": $localize`:Filter label:Status`, - "type": "prebuilt" + "id": "warningLevel" }, { "id": "date", 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 ec0f8edf82..61e485f976 100644 --- a/src/app/core/entity-list/EntityListConfig.ts +++ b/src/app/core/entity-list/EntityListConfig.ts @@ -25,7 +25,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; @@ -57,7 +57,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; @@ -84,7 +84,6 @@ export interface BasicFilterConfig { export interface BooleanFilterConfig extends BasicFilterConfig { true: string; false: string; - all: string; } export interface PrebuiltFilterConfig extends BasicFilterConfig { diff --git a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts index 0c95b72d70..8288714b8d 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts +++ b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts @@ -61,7 +61,6 @@ describe("EntityListComponent", () => { default: "true", true: "Currently active children", false: "Currently inactive children", - all: "All children", } as BooleanFilterConfig, { id: "center", 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..1b33351185 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; @@ -45,7 +42,6 @@ describe("FilterGeneratorService", () => { id: "privateSchool", true: "Private", false: "Government", - all: "All", type: "boolean", }; const schema = School.schema.get("privateSchool"); @@ -61,7 +57,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 +66,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 +125,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 +155,12 @@ 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 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 +189,6 @@ describe("FilterGeneratorService", () => { }); expect(comparableOptions).toEqual( jasmine.arrayWithExactContents([ - { key: "", label: "All" }, { key: "muslim", label: "muslim" }, { key: "christian", label: "christian" }, ]), @@ -218,7 +203,6 @@ describe("FilterGeneratorService", () => { label: "Date", default: "today", options: [ - { key: "", label: "All", filter: {} }, { key: "today", label: "Today", @@ -239,15 +223,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.selectedOptionValues).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 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..f1ae794197 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.selectedOptionValues = [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 455b878b9e..c4b9d49cd0 100644 --- a/src/app/core/filter/filter.service.ts +++ b/src/app/core/filter/filter.service.ts @@ -10,8 +10,8 @@ import { } from "@ucast/mongo2js"; import moment from "moment"; import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service"; +import { DataFilter, Filter as EntityFilter } from "./filters/filters"; import { MongoQuery } from "@casl/ability"; -import { DataFilter } from "./filters/filters"; /** * Utility service to help handling and aligning filters with entities. @@ -28,6 +28,22 @@ export class FilterService { constructor(private enumService: ConfigurableEnumService) {} + combineFilters( + entityFilters: EntityFilter[], + ): DataFilter { + if (entityFilters.length === 0) { + return {} as DataFilter; + } + + return { + $and: [ + ...entityFilters.map((value: EntityFilter): DataFilter => { + return value.getFilter(); + }), + ], + } as unknown as DataFilter; + } + /** * Builds a predicate for a given filter object. * This predicate can be used to filter arrays of objects. @@ -45,7 +61,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..3f3395c447 100644 --- a/src/app/core/filter/filter/filter.component.html +++ b/src/app/core/filter/filter/filter.component.html @@ -2,7 +2,7 @@ { let component: FilterComponent; let fixture: ComponentFixture; let loader: HarnessLoader; + let activatedRouteMock = new ActivatedRouteMock(); + beforeEach(async () => { + activatedRouteMock.snapshot = { + queryParams: {}, + }; + await TestBed.configureTestingModule({ imports: [FilterComponent, MockedTestingModule.withState()], + providers: [ + { + provide: ActivatedRoute, + useValue: activatedRouteMock, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(FilterComponent); @@ -28,13 +47,82 @@ describe("FilterComponent", () => { expect(component).toBeTruthy(); }); + it("should have no filter selected when url params are empty", async () => { + component.entityType = Note; + component.useUrlQueryParams = true; + component.filterConfig = [{ id: "category" }]; + + await component.ngOnChanges({ filterConfig: true } as any); + + expect(component.filterSelections.length).toBe(1); + expect(component.filterSelections[0].name).toBe("category"); + expect(component.filterSelections[0].selectedOptionValues).toBeEmpty(); + }); + + it("should load url params and set single filter value", async () => { + component.entityType = Note; + component.useUrlQueryParams = true; + component.filterConfig = [{ id: "category" }]; + + activatedRouteMock.snapshot = { + queryParams: { + category: "foo", + }, + }; + + await component.ngOnChanges({ filterConfig: true } as any); + + expect(component.filterSelections.length).toBe(1); + expect(component.filterSelections[0].name).toBe("category"); + expect(component.filterSelections[0].selectedOptionValues.length).toBe(1); + expect(component.filterSelections[0].selectedOptionValues[0]).toBe("foo"); + }); + + it("should load url params and set multiple filter value", async () => { + component.entityType = Note; + component.useUrlQueryParams = true; + component.filterConfig = [{ id: "category" }]; + + activatedRouteMock.snapshot = { + queryParams: { + category: "foo,bar", + }, + }; + + await component.ngOnChanges({ filterConfig: true } as any); + + expect(component.filterSelections.length).toBe(1); + expect(component.filterSelections[0].name).toBe("category"); + expect(component.filterSelections[0].selectedOptionValues.length).toBe(2); + expect(component.filterSelections[0].selectedOptionValues[0]).toBe("foo"); + expect(component.filterSelections[0].selectedOptionValues[1]).toBe("bar"); + }); + + it("should load url params and set no filter value when empty", async () => { + component.entityType = Note; + component.useUrlQueryParams = true; + component.filterConfig = [{ id: "category" }]; + + activatedRouteMock.snapshot = { + queryParams: { + category: "", + }, + }; + + await component.ngOnChanges({ filterConfig: true } as any); + + expect(component.filterSelections.length).toBe(1); + expect(component.filterSelections[0].name).toBe("category"); + expect(component.filterSelections[0].selectedOptionValues).toBeEmpty(); + }); + 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 +132,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(); + await options[0].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 bfb27e8e9a..7c22b90d6c 100644 --- a/src/app/core/filter/filter/filter.component.ts +++ b/src/app/core/filter/filter/filter.component.ts @@ -9,12 +9,13 @@ import { import { FilterConfig } from "../../entity-list/EntityListConfig"; import { Entity, EntityConstructor } from "../../entity/model/entity"; import { FilterGeneratorService } from "../filter-generator/filter-generator.service"; -import { ActivatedRoute, Params, Router } from "@angular/router"; -import { getUrlWithoutParams } from "../../../utils/utils"; +import { ActivatedRoute, Router } from "@angular/router"; import { ListFilterComponent } from "../list-filter/list-filter.component"; import { NgForOf, NgIf } from "@angular/common"; import { Angulartics2Module } from "angulartics2"; import { DateRangeFilterComponent } from "../../basic-datatypes/date/date-range-filter/date-range-filter.component"; +import { getUrlWithoutParams } from "../../../utils/utils"; +import { FilterService } from "../filter.service"; import { DataFilter, Filter } from "../filters/filters"; /** @@ -62,7 +63,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>(); @@ -71,6 +72,7 @@ export class FilterComponent implements OnChanges { constructor( private filterGenerator: FilterGeneratorService, + private filterService: FilterService, private router: Router, private route: ActivatedRoute, ) {} @@ -88,19 +90,19 @@ export class FilterComponent implements OnChanges { } } - filterOptionSelected(filter: Filter, selectedOption: string) { - filter.selectedOption = selectedOption; + filterOptionSelected(filter: Filter, selectedOptions: string[]) { + filter.selectedOptionValues = 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)) { @@ -121,14 +123,16 @@ export class FilterComponent implements OnChanges { }); } - private loadUrlParams(parameters?: Params) { + private loadUrlParams() { if (!this.useUrlQueryParams) { return; } - const params = parameters || this.route.snapshot.queryParams; + const params = this.route.snapshot.queryParams; this.filterSelections.forEach((f) => { if (params.hasOwnProperty(f.name)) { - f.selectedOption = params[f.name]; + f.selectedOptionValues = params[f.name] + .split(",") + .filter((value) => value !== ""); } }); } diff --git a/src/app/core/filter/filters/booleanFilter.ts b/src/app/core/filter/filters/booleanFilter.ts new file mode 100644 index 0000000000..da9629ebfc --- /dev/null +++ b/src/app/core/filter/filters/booleanFilter.ts @@ -0,0 +1,28 @@ +import { Entity } from "../../entity/model/entity"; +import { BooleanFilterConfig } from "../../entity-list/EntityListConfig"; +import { DataFilter, SelectableFilter } from "./filters"; + +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..9cf50edde4 --- /dev/null +++ b/src/app/core/filter/filters/configurableEnumFilter.ts @@ -0,0 +1,23 @@ +import { Entity } from "../../entity/model/entity"; +import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface"; +import { DataFilter, FilterSelectionOption, SelectableFilter } from "./filters"; + +export class ConfigurableEnumFilter< + T extends Entity, +> extends SelectableFilter { + constructor( + name: string, + label: string, + enumValues: ConfigurableEnumValue[], + ) { + const options: FilterSelectionOption[] = 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..a25ae6e19b --- /dev/null +++ b/src/app/core/filter/filters/dateFilter.ts @@ -0,0 +1,71 @@ +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 { DataFilter, Filter } from "./filters"; +import { isValidDate } from "../../../utils/utils"; + +/** + * 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.selectedOptionValues = []; + } + + /** + * Returns the date range according to the selected option or dates + */ + getDateRange(): DateRange { + const selectedOption = this.getSelectedOption(); + if (selectedOption) { + return calculateDateRange(selectedOption); + } + const dates = this.selectedOptionValues; + if (dates?.length == 2) { + return this.getDateRangeFromDateStrings(dates[0], dates[1]); + } + 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.selectedOptionValues as any]; + } + + private getDateRangeFromDateStrings( + dateStr1: string, + dateStr2: string, + ): DateRange { + const date1 = moment(dateStr1).toDate(); + const date2 = moment(dateStr2).toDate(); + + return new DateRange( + isValidDate(date1) ? date1 : undefined, + isValidDate(date2) ? date2 : undefined, + ); + } +} 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..d94d74333f 100644 --- a/src/app/core/filter/filters/filters.spec.ts +++ b/src/app/core/filter/filters/filters.spec.ts @@ -1,11 +1,13 @@ -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[], ) { @@ -25,24 +27,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.selectedOptionValues = ["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"); }); @@ -53,32 +55,21 @@ describe("Filters", () => { default: "true", true: "is true", false: "is not true", - all: "All", }); const recordTrue = { value: true }; const recordFalse = { value: false }; - const recordUndefined = {}; - filter.selectedOption = "true"; - testFilter( - filter, - [recordFalse, recordTrue, recordUndefined], - [recordTrue], - ); + filter.selectedOptionValues = ["true"]; + testFilter(filter, [recordFalse, recordTrue], [recordTrue]); - filter.selectedOption = "false"; - testFilter( - filter, - [recordFalse, recordTrue, recordUndefined], - [recordFalse, recordUndefined], - ); + filter.selectedOptionValues = ["false"]; + testFilter(filter, [recordFalse, recordTrue], [recordFalse]); - filter.selectedOption = "all"; - testFilter( - filter, - [recordFalse, recordTrue, recordUndefined], - [recordFalse, recordTrue, recordUndefined], - ); + filter.selectedOptionValues = []; + testFilter(filter, [recordFalse, recordTrue], [recordFalse, recordTrue]); + + filter.selectedOptionValues = ["true", "false"]; + testFilter(filter, [recordFalse, recordTrue], [recordFalse, recordTrue]); }); }); diff --git a/src/app/core/filter/filters/filters.ts b/src/app/core/filter/filters/filters.ts index 9ed1a92ba2..9a4a6feab1 100644 --- a/src/app/core/filter/filters/filters.ts +++ b/src/app/core/filter/filters/filters.ts @@ -15,16 +15,7 @@ * 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 { 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"; import { MongoQuery } from "@casl/ability"; /** @@ -36,9 +27,9 @@ import { MongoQuery } from "@casl/ability"; export type DataFilter = MongoQuery | {}; export abstract class Filter { - public selectedOption: string; + public selectedOptionValues: string[] = []; - constructor( + protected constructor( public name: string, public label: string = name, ) {} @@ -46,61 +37,6 @@ export abstract class Filter { 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]; - } -} - /** * Generic configuration for a filter with different selectable {@link FilterSelectionOption} options. * @@ -127,25 +63,13 @@ 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, - }, - ]; - - options.push( - ...valuesToMatchAsOptions - .filter((k) => !!k) - .map((k) => ({ - key: k.toLowerCase(), - label: k.toString(), - filter: { [attributeName]: k } as DataFilter, - })), - ); - - return options; + return valuesToMatchAsOptions + .filter((k) => !!k) + .map((k) => ({ + key: k.toLowerCase(), + label: k.toString(), + filter: { [attributeName]: k } as DataFilter, + })); } /** @@ -161,18 +85,17 @@ export class SelectableFilter extends Filter { public label: string = name, ) { super(name, label); - this.selectedOption = this.options[0]?.key; + this.selectedOptionValues = []; } - /** 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; + }); } /** @@ -180,94 +103,19 @@ export class SelectableFilter extends Filter { * 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; + const filters: DataFilter[] = this.selectedOptionValues + .map((value: string) => this.getOption(value)) + .filter((value: FilterSelectionOption) => value !== undefined) + .map((previousValue: FilterSelectionOption) => { + return previousValue.filter as DataFilter; + }); + + if (filters.length === 0) { + return {} as DataFilter; } - } -} - -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: {}, - }, - ]; - 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 { + $or: [...filters], + } as unknown as DataFilter; } } @@ -288,5 +136,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 e4834c3190..81b7b2c3db 100644 --- a/src/app/features/todos/todo-list/todo-list.component.ts +++ b/src/app/features/todos/todo-list/todo-list.component.ts @@ -4,7 +4,10 @@ import { PrebuiltFilterConfig } from "../../../core/entity-list/EntityListConfig import { TodoDetailsComponent } from "../todo-details/todo-details.component"; import moment from "moment"; import { EntityListComponent } from "../../../core/entity-list/entity-list/entity-list.component"; -import { FilterSelectionOption } from "../../../core/filter/filters/filters"; +import { + DataFilter, + FilterSelectionOption, +} from "../../../core/filter/filters/filters"; import { RouteTarget } from "../../../route-target"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Sort } from "@angular/material/sort"; @@ -213,5 +216,5 @@ const filterCurrentlyActive: FilterSelectionOption = { ], }, ], - }, + } as DataFilter, };