Skip to content

Commit

Permalink
fix(filter): date-range filter available for any field (#2307)
Browse files Browse the repository at this point in the history
and refactoring of filter-component

closes #2292
  • Loading branch information
sleidig authored Mar 20, 2024
1 parent f424d13 commit c51c818
Show file tree
Hide file tree
Showing 11 changed files with 70 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,6 @@ describe("NotesManagerComponent", () => {
expect(component).toBeTruthy();
});

it("should set up prebuilt filters", fakeAsync(() => {
component.ngOnInit();
tick();
expect(component.config.filters.length).toEqual(3);
expect(component.config.filters[0]).toHaveOwnProperty("options");
expect(component.config.filters[1]).toHaveOwnProperty("options");
expect(component.config.filters[2]).not.toHaveOwnProperty("options");
}));

it("should open the dialog when clicking details", () => {
const note = new Note("testNote");
component.showDetails(note);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,14 @@ 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 {
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";
import { EntityListComponent } from "../../../core/entity-list/entity-list/entity-list.component";
import { applyUpdate } from "../../../core/entity/model/entity-update";
import { EntityListConfig } from "../../../core/entity-list/EntityListConfig";
import { EventNote } from "../../attendance/model/event-note";
import { DynamicComponentConfig } from "../../../core/config/dynamic-components/dynamic-component-config.interface";
import { merge } from "rxjs";
import moment from "moment";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { NgIf } from "@angular/common";
import { FormsModule } from "@angular/forms";
Expand Down Expand Up @@ -60,25 +54,10 @@ export class NotesManagerComponent implements OnInit {
entityConstructor = Note;
notes: Note[];

private dateFS: FilterSelectionOption<Note>[] = [
{
key: "current-week",
label: $localize`:Filter-option for notes:This Week`,
filter: { date: this.getWeeksFilter(0) } as DataFilter<any>,
},
{
key: "last-week",
label: $localize`:Filter-option for notes:Since Last Week`,
filter: { date: this.getWeeksFilter(1) } as DataFilter<any>,
},
{ key: "", label: $localize`All`, filter: {} },
];

constructor(
private formDialog: FormDialogService,
private entityMapperService: EntityMapperService,
private route: ActivatedRoute,
private log: LoggingService,
) {}

async ngOnInit() {
Expand All @@ -88,7 +67,6 @@ export class NotesManagerComponent implements OnInit {
) => {
// TODO replace this use of route and rely on the RoutedViewComponent instead
this.config = data.config;
this.addPrebuiltFilters();
this.notes = await this.loadEntities();
},
);
Expand Down Expand Up @@ -128,35 +106,6 @@ export class NotesManagerComponent implements OnInit {
this.notes = await this.loadEntities();
}

private addPrebuiltFilters() {
for (const prebuiltFilter of this.config.filters.filter(
(filter) => filter.type === "prebuilt",
)) {
switch (prebuiltFilter.id) {
case "date": {
prebuiltFilter["options"] = this.dateFS;
prebuiltFilter["default"] = "current-week";
break;
}
default: {
this.log.warn(
"[NoteManagerComponent] No filter options available for prebuilt filter: " +
prebuiltFilter.id,
);
prebuiltFilter["options"] = [];
}
}
}
}

private getWeeksFilter(weeksBack: number) {
const start = moment().subtract(weeksBack, "weeks").startOf("week");
const end = moment().endOf("day");
const startString = start.format("YYYY-MM-DD");
const endString = end.format("YYYY-MM-DD");
return { $gte: startString, $lte: endString };
}

addNoteClick() {
const newNote = new Note(Date.now().toString());
this.showDetails(newNote);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<mat-form-field>
<mat-label i18n="Input label">Enter a date range</mat-label>
<mat-label>{{ filterConfig.label || filterConfig.name }}</mat-label>

<mat-date-range-input>
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ describe("DateRangeFilterComponent", () => {

fixture = TestBed.createComponent(DateRangeFilterComponent);
component = fixture.componentInstance;

component.filterConfig = new DateFilter<any>("test", "test label", []);

fixture.detectChanges();
});

Expand All @@ -32,7 +35,7 @@ describe("DateRangeFilterComponent", () => {

dateFilter.selectedOptionValues = ["9"];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
expect(component.filterConfig.getFilter()).toEqual({});

jasmine.clock().mockDate(moment("2023-05-18").toDate());
dateFilter.selectedOptionValues = ["0"];
Expand All @@ -43,7 +46,7 @@ describe("DateRangeFilterComponent", () => {
$lte: "2023-05-18",
},
};
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter);
expect(component.filterConfig.getFilter()).toEqual(expectedDataFilter);

dateFilter.selectedOptionValues = ["1"];
component.filterConfig = dateFilter;
Expand All @@ -53,11 +56,11 @@ describe("DateRangeFilterComponent", () => {
$lte: "2023-05-20",
},
};
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter);
expect(component.filterConfig.getFilter()).toEqual(expectedDataFilter);

dateFilter.selectedOptionValues = [];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
expect(component.filterConfig.getFilter()).toEqual({});
jasmine.clock().uninstall();
});

Expand All @@ -66,27 +69,27 @@ describe("DateRangeFilterComponent", () => {

dateFilter.selectedOptionValues = ["1", "2", "3"];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
expect(component.filterConfig.getFilter()).toEqual({});

dateFilter.selectedOptionValues = [];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
expect(component.filterConfig.getFilter()).toEqual({});

dateFilter.selectedOptionValues = ["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.filterConfig.getFilter()).toEqual(expectedDateFilter);

dateFilter.selectedOptionValues = ["", "2023-01-3"];
component.filterConfig = dateFilter;
testFilter = { $lte: "2023-01-03" };
expectedDateFilter = {
test: testFilter,
};
expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter);
expect(component.filterConfig.getFilter()).toEqual(expectedDateFilter);

dateFilter.selectedOptionValues = ["2022-9-18", "2023-01-3"];
component.filterConfig = dateFilter;
Expand All @@ -97,7 +100,7 @@ describe("DateRangeFilterComponent", () => {
expectedDateFilter = {
test: testFilter,
};
expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter);
expect(component.filterConfig.getFilter()).toEqual(expectedDateFilter);
});

it("should set the correct date filter when changing the date range manually", () => {
Expand All @@ -107,7 +110,7 @@ describe("DateRangeFilterComponent", () => {

component.dateChangedManually();

expect(component.dateFilter.selectedOptionValues).toEqual([
expect(component.filterConfig.selectedOptionValues).toEqual([
"2021-10-28",
"2024-02-12",
]);
Expand All @@ -117,6 +120,6 @@ describe("DateRangeFilterComponent", () => {
$lte: "2024-02-12",
},
};
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter);
expect(component.filterConfig.getFilter()).toEqual(expectedDataFilter);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { Entity } from "../../../entity/model/entity";
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";
Expand All @@ -16,38 +15,42 @@ import { DateFilter } from "../../../filter/filters/dateFilter";
standalone: true,
imports: [MatFormFieldModule, MatDatepickerModule, FormsModule],
})
export class DateRangeFilterComponent<T extends Entity> {
export class DateRangeFilterComponent<T extends Entity> implements OnChanges {
fromDate: Date;
toDate: Date;
dateFilter: DateFilter<T>;

@Output() selectedOptionChange = new EventEmitter<string[]>();

@Input() set filterConfig(value: Filter<T>) {
this.dateFilter = value as DateFilter<T>;
this.initDates();
}
@Input() filterConfig: DateFilter<T>;

constructor(private dialog: MatDialog) {}

ngOnChanges(changes: SimpleChanges): void {
if (changes.filterConfig) {
this.initDates();
}
}

private initDates() {
const range = this.dateFilter.getDateRange();
const range = this.filterConfig.getDateRange();
if (
(range.start !== this.fromDate || range.start === undefined) &&
(range.end !== this.toDate || range.end === undefined)
) {
this.fromDate = range.start;
this.toDate = range.end;
this.selectedOptionChange.emit(this.dateFilter.selectedOptionValues);
this.filterConfig.selectedOptionChange.emit(
this.filterConfig.selectedOptionValues,
);
}
}

dateChangedManually() {
this.dateFilter.selectedOptionValues = [
this.filterConfig.selectedOptionValues = [
isValidDate(this.fromDate) ? dateToString(this.fromDate) : "",
isValidDate(this.toDate) ? dateToString(this.toDate) : "",
];
this.selectedOptionChange.emit(this.dateFilter.selectedOptionValues);
this.filterConfig.selectedOptionChange.emit(
this.filterConfig.selectedOptionValues,
);
}

openDialog(e: Event) {
Expand All @@ -56,7 +59,7 @@ export class DateRangeFilterComponent<T extends Entity> {
.open(DateRangeFilterPanelComponent, {
width: "600px",
minWidth: "400px",
data: this.dateFilter,
data: this.filterConfig,
})
.afterClosed()
.subscribe(() => this.initDates());
Expand Down
25 changes: 5 additions & 20 deletions src/app/core/filter/filter/filter.component.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
<ng-container *ngFor="let filter of filterSelections">
<app-list-filter
*ngIf="filter.name !== 'date'"
[filterConfig]="filter"
[selectedOptions]="filter.selectedOptionValues"
(selectedOptionChange)="filterOptionSelected(filter, $event)"
angulartics2On="click"
[angularticsCategory]="entityType?.ENTITY_TYPE"
[angularticsAction]="'filter_' + urlPath"
[angularticsLabel]="filter.label"
>
</app-list-filter>

<app-date-range-filter
*ngIf="filter.name === 'date'"
[filterConfig]="filter"
(selectedOptionChange)="filterOptionSelected(filter, $event)"
>
</app-date-range-filter>
</ng-container>
@for (filter of filterSelections; track filter.name) {
<ng-container
*ngComponentOutlet="filter.component; inputs: { filterConfig: filter }"
/>
}
19 changes: 8 additions & 11 deletions src/app/core/filter/filter/filter.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import { FilterConfig } from "../../entity-list/EntityListConfig";
import { Entity, EntityConstructor } from "../../entity/model/entity";
import { FilterGeneratorService } from "../filter-generator/filter-generator.service";
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 { NgComponentOutlet } from "@angular/common";
import { getUrlWithoutParams } from "../../../utils/utils";
import { FilterService } from "../filter.service";
import { DataFilter, Filter } from "../filters/filters";
Expand All @@ -24,13 +21,7 @@ import { DataFilter, Filter } from "../filters/filters";
@Component({
selector: "app-filter",
templateUrl: "./filter.component.html",
imports: [
ListFilterComponent,
NgForOf,
Angulartics2Module,
DateRangeFilterComponent,
NgIf,
],
imports: [NgComponentOutlet],
standalone: true,
})
export class FilterComponent<T extends Entity = Entity> implements OnChanges {
Expand Down Expand Up @@ -85,6 +76,12 @@ export class FilterComponent<T extends Entity = Entity> implements OnChanges {
this.entities,
this.onlyShowRelevantFilterOptions,
);
for (const filter of this.filterSelections) {
filter.selectedOptionChange.subscribe((event) =>
this.filterOptionSelected(filter, event),
);
}

this.loadUrlParams();
this.applyFilterSelections();
}
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/filter/filters/dateFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { calculateDateRange } from "../../basic-datatypes/date/date-range-filter
import moment from "moment";
import { DataFilter, Filter } from "./filters";
import { isValidDate } from "../../../utils/utils";
import { DateRangeFilterComponent } from "../../basic-datatypes/date/date-range-filter/date-range-filter.component";

/**
* Represents a filter for date values.
* The filter can either be one of the predefined options or two manually entered dates.
*/
export class DateFilter<T extends Entity> extends Filter<T> {
override component = DateRangeFilterComponent;

constructor(
public name: string,
public label: string = name,
Expand Down
15 changes: 15 additions & 0 deletions src/app/core/filter/filters/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import { Entity } from "../../entity/model/entity";
import { MongoQuery } from "@casl/ability";
import { ListFilterComponent } from "../list-filter/list-filter.component";
import { EventEmitter, Type } from "@angular/core";

/**
* This filter can be used to filter an array of entities.
Expand All @@ -27,8 +29,21 @@ import { MongoQuery } from "@casl/ability";
export type DataFilter<T> = MongoQuery<T> | {};

export abstract class Filter<T extends Entity> {
/**
* The component used to display filter option to the user.
*/
component: Type<any> = ListFilterComponent;

public selectedOptionValues: string[] = [];

/**
* Triggered when this filter changes value
* (e.g. when the user selects a new value in a FilterComponent).
*
* This is part of the filter object because dynamic filter components can't expose @Outputs
*/
selectedOptionChange = new EventEmitter<string[]>();

protected constructor(
public name: string,
public label: string = name,
Expand Down
Loading

0 comments on commit c51c818

Please sign in to comment.