-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: configurable date range filter for lists (#1487)
closes #1448, #1383 MIGRATION REQUIRED: "type": "prebuilt" has to be removed from existing filter configs. Co-authored-by: Sebastian <sleidig@users.noreply.github.com> Co-authored-by: Sebastian Leidig <sebastian.leidig@gmail.com> Co-authored-by: Simon <simon@aam-digital.com>
- 3.44.1-master.3
- 3.44.1-master.2
- 3.44.1-master.1
- 3.44.0
- 3.43.1
- 3.43.0
- 3.42.2
- 3.42.1
- 3.42.0
- 3.41.2
- 3.41.1
- 3.41.0
- 3.40.0
- 3.39.3
- 3.39.2
- 3.39.1
- 3.39.0
- 3.38.2
- 3.38.1
- 3.38.0
- 3.37.0
- 3.36.1
- 3.36.0
- 3.35.0
- 3.34.2
- 3.34.1
- 3.34.0
- 3.33.0
- 3.32.3
- 3.32.2
- 3.32.1
- 3.32.0
- 3.31.1
- 3.31.0
- 3.30.1
- 3.30.0
- 3.29.0
- 3.28.0
- 3.27.2
- 3.27.1
- 3.27.0
- 3.26.1
- 3.26.1-master.3
- 3.26.1-master.2
- 3.26.1-master.1
- 3.26.0
- 3.26.0-master.23
- 3.26.0-master.22
- 3.26.0-master.21
- 3.26.0-master.20
- 3.26.0-master.19
- 3.26.0-master.18
- 3.26.0-master.17
- 3.26.0-master.16
- 3.26.0-master.15
- 3.26.0-master.14
- 3.26.0-master.13
- 3.26.0-master.12
- 3.26.0-master.11
- 3.26.0-master.10
- 3.26.0-master.9
- 3.26.0-master.8
- 3.26.0-master.7
- 3.26.0-master.6
- 3.26.0-master.5
- 3.26.0-master.4
- 3.26.0-master.3
- 3.26.0-master.2
- 3.26.0-master.1
- 3.25.1
- 3.25.1-master.2
- 3.25.1-master.1
- 3.25.0
- 3.25.0-master.2
- 3.25.0-master.1
- 3.24.1-master.2
- 3.24.1-master.1
- 3.24.0
- 3.24.0-master.12
- 3.24.0-master.11
- 3.24.0-master.10
- 3.24.0-master.9
- 3.24.0-master.8
- 3.24.0-master.7
- 3.24.0-master.6
- 3.24.0-master.5
- 3.24.0-master.4
- 3.24.0-master.3
- 3.24.0-master.2
- 3.24.0-master.1
- 3.23.0
- 3.23.0-master.7
- 3.23.0-master.6
- 3.23.0-master.5
- 3.23.0-master.4
- 3.23.0-master.3
- 3.23.0-master.2
- 3.23.0-master.1
- 3.22.1-master.5
- 3.22.1-master.4
- 3.22.1-master.3
- 3.22.1-master.2
- 3.22.1-master.1
- 3.22.0
- 3.22.0-master.12
- 3.22.0-master.11
- 3.22.0-master.10
- 3.22.0-master.9
- 3.22.0-master.8
- 3.22.0-master.7
- 3.22.0-master.6
- 3.22.0-master.5
- 3.22.0-master.4
- 3.22.0-master.3
- 3.22.0-master.2
1 parent
a689b02
commit bffeb5c
Showing
32 changed files
with
1,061 additions
and
441 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 0 additions & 29 deletions
29
src/app/core/entity-components/entity-list/filter-component.settings.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
226 changes: 78 additions & 148 deletions
226
src/app/core/entity-components/entity-list/filter-generator.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,190 +1,120 @@ | ||
import { Injectable } from "@angular/core"; | ||
import { | ||
FilterSelection, | ||
FilterSelectionOption, | ||
} from "../../filter/filter-selection/filter-selection"; | ||
DateFilter, | ||
SelectableFilter, | ||
BooleanFilter, | ||
ConfigurableEnumFilter, | ||
EntityFilter, | ||
Filter, | ||
} from "../../filter/filters/filters"; | ||
import { | ||
BooleanFilterConfig, | ||
DateRangeFilterConfig, | ||
FilterConfig, | ||
PrebuiltFilterConfig, | ||
} from "./EntityListConfig"; | ||
import { Entity, EntityConstructor } from "../../entity/model/entity"; | ||
import { LoggingService } from "../../logging/logging.service"; | ||
import { EntitySchemaField } from "../../entity/schema/entity-schema-field"; | ||
import { FilterComponentSettings } from "./filter-component.settings"; | ||
import { EntityMapperService } from "../../entity/entity-mapper.service"; | ||
import { EntityRegistry } from "../../entity/database-entity.decorator"; | ||
import { FilterService } from "../../filter/filter.service"; | ||
import { ConfigurableEnumService } from "../../configurable-enum/configurable-enum.service"; | ||
import { FilterService } from "app/core/filter/filter.service"; | ||
import { defaultDateFilters } from "../../filter/date-range-filter/date-range-filter-panel/date-range-filter-panel.component"; | ||
import { dateDataTypes } from "../../entity/schema-datatypes/date-datatypes"; | ||
|
||
@Injectable({ | ||
providedIn: "root", | ||
}) | ||
export class FilterGeneratorService { | ||
constructor( | ||
private enumService: ConfigurableEnumService, | ||
private loggingService: LoggingService, | ||
private entities: EntityRegistry, | ||
private entityMapperService: EntityMapperService, | ||
private filterService: FilterService | ||
) {} | ||
|
||
/** | ||
* | ||
* @param filtersConfig | ||
* @param filterConfigs | ||
* @param entityConstructor | ||
* @param data | ||
* @param onlyShowUsedOptions (Optional) whether to remove those filter options for selection that are not present in the data | ||
*/ | ||
async generate<T extends Entity>( | ||
filtersConfig: FilterConfig[], | ||
filterConfigs: FilterConfig[], | ||
entityConstructor: EntityConstructor<T>, | ||
data: T[], | ||
onlyShowUsedOptions = false | ||
): Promise<FilterComponentSettings<T>[]> { | ||
const filterSettings: FilterComponentSettings<T>[] = []; | ||
for (const filter of filtersConfig) { | ||
const schema = entityConstructor.schema.get(filter.id) || {}; | ||
const fs: FilterComponentSettings<T> = { | ||
filterSettings: new FilterSelection( | ||
filter.id, | ||
[], | ||
filter.label || schema.label | ||
), | ||
}; | ||
try { | ||
fs.filterSettings.options = await this.getFilterOptions( | ||
filter, | ||
schema, | ||
data | ||
): Promise<Filter<T>[]> { | ||
const filters: Filter<T>[] = []; | ||
for (const filterConfig of filterConfigs) { | ||
const schema = entityConstructor.schema.get(filterConfig.id) || {}; | ||
let filter: Filter<T>; | ||
const type = filterConfig.type ?? schema.dataType; | ||
if ( | ||
type == "configurable-enum" || | ||
schema.innerDataType === "configurable-enum" | ||
) { | ||
filter = new ConfigurableEnumFilter( | ||
filterConfig.id, | ||
filterConfig.label || schema.label, | ||
this.enumService.getEnumValues( | ||
schema.additional ?? schema.innerDataType | ||
) | ||
); | ||
} catch (e) { | ||
this.loggingService.warn(`Could not init filter: ${filter.id}: ${e}`); | ||
} | ||
|
||
if (onlyShowUsedOptions) { | ||
fs.filterSettings.options = fs.filterSettings.options.filter((option) => | ||
data.some(this.filterService.getFilterPredicate(option.filter)) | ||
} else if (type == "boolean") { | ||
filter = new BooleanFilter( | ||
filterConfig.id, | ||
filterConfig.label || schema.label, | ||
filterConfig as BooleanFilterConfig | ||
); | ||
} else if (type == "prebuilt") { | ||
filter = new SelectableFilter( | ||
filterConfig.id, | ||
(filterConfig as PrebuiltFilterConfig<T>).options, | ||
filterConfig.label | ||
); | ||
} else if (dateDataTypes.includes(type)) { | ||
filter = new DateFilter( | ||
filterConfig.id, | ||
filterConfig.label || schema.label, | ||
(filterConfig as DateRangeFilterConfig).options ?? defaultDateFilters | ||
); | ||
} else if ( | ||
this.entities.has(filterConfig.type) || | ||
this.entities.has(schema.additional) | ||
) { | ||
const entityType = filterConfig.type || schema.additional; | ||
const filterEntities = await this.entityMapperService.loadType( | ||
entityType | ||
); | ||
filter = new EntityFilter(filterConfig.id, entityType, filterEntities); | ||
} else { | ||
const options = [...new Set(data.map((c) => c[filterConfig.id]))]; | ||
const fSO = SelectableFilter.generateOptions(options, filterConfig.id); | ||
filter = new SelectableFilter<T>( | ||
filterConfig.id, | ||
fSO, | ||
filterConfig.label || schema.label | ||
); | ||
} | ||
|
||
// Filters should only be added, if they have more than one (the default) option | ||
if (fs.filterSettings.options?.length > 1) { | ||
fs.selectedOption = filter.hasOwnProperty("default") | ||
? filter.default | ||
: fs.filterSettings.options[0].key; | ||
filterSettings.push(fs); | ||
if (filterConfig.hasOwnProperty("default")) { | ||
filter.selectedOption = filterConfig.default; | ||
} | ||
} | ||
return filterSettings; | ||
} | ||
|
||
private async getFilterOptions<T extends Entity>( | ||
config: FilterConfig, | ||
schema: EntitySchemaField, | ||
data: T[] | ||
): Promise<FilterSelectionOption<T>[]> { | ||
if (config.type === "prebuilt") { | ||
return (config as PrebuiltFilterConfig<T>).options; | ||
} else if (schema.dataType === "boolean" || config.type === "boolean") { | ||
return this.createBooleanFilterOptions(config as BooleanFilterConfig); | ||
} else if ( | ||
schema.dataType === "configurable-enum" || | ||
schema.innerDataType === "configurable-enum" | ||
) { | ||
return this.createConfigurableEnumFilterOptions( | ||
config.id, | ||
schema.additional ?? schema.innerDataType | ||
); | ||
} else if ( | ||
this.entities.has(config.type) || | ||
this.entities.has(schema.additional) | ||
) { | ||
return this.createEntityFilterOption( | ||
config.id, | ||
config.type || schema.additional | ||
); | ||
} else { | ||
const options = [...new Set(data.map((c) => c[config.id]))]; | ||
return FilterSelection.generateOptions(options, config.id); | ||
} | ||
} | ||
|
||
private createBooleanFilterOptions<T extends Entity>( | ||
filter: BooleanFilterConfig | ||
): FilterSelectionOption<T>[] { | ||
return [ | ||
{ | ||
key: "all", | ||
label: filter.all ?? $localize`:Filter label:All`, | ||
filter: {}, | ||
}, | ||
{ | ||
key: "true", | ||
label: filter.true ?? $localize`:Filter label default boolean true:Yes`, | ||
filter: { [filter.id]: true }, | ||
}, | ||
{ | ||
key: "false", | ||
label: filter.false ?? $localize`:Filter label default boolean true:No`, | ||
filter: { [filter.id]: false }, | ||
}, | ||
]; | ||
} | ||
|
||
private createConfigurableEnumFilterOptions<T extends Entity>( | ||
property: string, | ||
enumId: string | ||
): FilterSelectionOption<T>[] { | ||
const options: FilterSelectionOption<T>[] = [ | ||
{ | ||
key: "all", | ||
label: $localize`:Filter label:All`, | ||
filter: {}, | ||
}, | ||
]; | ||
|
||
const enumValues = this.enumService.getEnumValues(enumId); | ||
const key = property + ".id"; | ||
|
||
for (const enumValue of enumValues) { | ||
options.push({ | ||
key: enumValue.id, | ||
label: enumValue.label, | ||
color: enumValue.color, | ||
filter: { [key]: enumValue.id }, | ||
}); | ||
if (filter instanceof SelectableFilter) { | ||
if (onlyShowUsedOptions) { | ||
filter.options = filter.options.filter((option) => | ||
data.some(this.filterService.getFilterPredicate(option.filter)) | ||
); | ||
} | ||
// Filters should only be added, if they have more than one (the default) option | ||
if (filter.options?.length <= 1) { | ||
continue; | ||
} | ||
} | ||
filters.push(filter); | ||
} | ||
|
||
return options; | ||
} | ||
|
||
private async createEntityFilterOption<T extends Entity>( | ||
property: string, | ||
entityType: string | ||
): Promise<FilterSelectionOption<T>[]> { | ||
const filterEntities = await this.entityMapperService.loadType(entityType); | ||
filterEntities.sort((a, b) => a.toString().localeCompare(b.toString())); | ||
|
||
const options = [ | ||
{ | ||
key: "all", | ||
label: $localize`:Filter option:All`, | ||
filter: {}, | ||
}, | ||
]; | ||
options.push( | ||
...filterEntities.map((filterEntity) => ({ | ||
key: filterEntity.getId(), | ||
label: filterEntity.toString(), | ||
filter: { | ||
$or: [ | ||
{ [property]: filterEntity.getId() }, | ||
{ [property]: { $elemMatch: { $eq: filterEntity.getId() } } }, | ||
], | ||
}, | ||
})) | ||
); | ||
return options; | ||
return filters; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { dateEntitySchemaDatatype } from "./datatype-date"; | ||
import { dateOnlyEntitySchemaDatatype } from "./datatype-date-only"; | ||
import { monthEntitySchemaDatatype } from "./datatype-month"; | ||
import { dateWithAgeEntitySchemaDatatype } from "./datatype-date-with-age"; | ||
|
||
export const dateDataTypes = [ | ||
dateEntitySchemaDatatype, | ||
dateOnlyEntitySchemaDatatype, | ||
monthEntitySchemaDatatype, | ||
dateWithAgeEntitySchemaDatatype, | ||
].map((dataType) => dataType.name); |
31 changes: 31 additions & 0 deletions
31
...e/filter/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<div mat-dialog-title i18n="Title of dialog">Select a date range</div> | ||
<mat-dialog-content> | ||
<div class="container"> | ||
<mat-calendar | ||
class="calendar" | ||
(selectedChange)="selectedRangeChange($event)" | ||
[selected]="selectedRangeValue" | ||
[comparisonStart]="comparisonRange.start" | ||
[comparisonEnd]="comparisonRange.end" | ||
> | ||
</mat-calendar> | ||
<div class="panel"> | ||
<div | ||
class="button-div" | ||
*ngFor="let item of filter.rangeOptions; let selectedIndexOfDateRanges = index" | ||
> | ||
<button | ||
class="button" | ||
mat-button | ||
color="primary" | ||
[class.selected-option]="item === selectedOption" | ||
(mouseenter)="preselectRange(item)" | ||
(mouseleave)="unselectRange()" | ||
(click)="selectRangeAndClose(selectedIndexOfDateRanges)" | ||
> | ||
{{ item.label }} | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</mat-dialog-content> |
27 changes: 27 additions & 0 deletions
27
...e/filter/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
.container { | ||
display: flex; | ||
} | ||
|
||
.calendar { | ||
width: 60%; | ||
} | ||
|
||
.panel { | ||
width: 40%; | ||
margin: auto; | ||
} | ||
|
||
.button-div { | ||
display: block; | ||
text-align: left; | ||
line-height: 100%; | ||
} | ||
|
||
.button { | ||
display: block; | ||
text-align: left; | ||
} | ||
|
||
.selected-option { | ||
font-weight: bold; | ||
} |
157 changes: 157 additions & 0 deletions
157
...ilter/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { ComponentFixture, TestBed } from "@angular/core/testing"; | ||
|
||
import { | ||
DateRangeFilterPanelComponent, | ||
calculateDateRange, | ||
defaultDateFilters, | ||
} from "./date-range-filter-panel.component"; | ||
import { MatNativeDateModule } from "@angular/material/core"; | ||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; | ||
import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed"; | ||
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 "../../filters/filters"; | ||
|
||
describe("DateRangeFilterPanelComponent", () => { | ||
let component: DateRangeFilterPanelComponent; | ||
let fixture: ComponentFixture<DateRangeFilterPanelComponent>; | ||
let loader: HarnessLoader; | ||
let dateFilter: DateFilter<any>; | ||
|
||
beforeEach(async () => { | ||
dateFilter = new DateFilter("test", "Test", defaultDateFilters); | ||
dateFilter.selectedOption = "1"; | ||
jasmine.clock().mockDate(new Date("2023-04-08")); | ||
await TestBed.configureTestingModule({ | ||
imports: [MatNativeDateModule], | ||
providers: [ | ||
{ provide: MAT_DIALOG_DATA, useValue: dateFilter }, | ||
{ provide: MatDialogRef, useValue: { close: () => undefined } }, | ||
], | ||
}).compileComponents(); | ||
fixture = TestBed.createComponent(DateRangeFilterPanelComponent); | ||
loader = TestbedHarnessEnvironment.loader(fixture); | ||
component = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it("should create", () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
|
||
it("should highlight the currently selected option", () => { | ||
expect(component.selectedOption).toEqual(defaultDateFilters[1]); | ||
}); | ||
|
||
it("should display selected dates in the calendar", async () => { | ||
const fromDate = moment().startOf("month"); | ||
const toDate = moment().startOf("month").add(13, "days"); | ||
component.selectedRangeValue = new DateRange( | ||
fromDate.toDate(), | ||
toDate.toDate() | ||
); | ||
fixture.detectChanges(); | ||
const calendar = await loader.getHarness(MatCalendarHarness); | ||
const cells = await calendar.getCells(); | ||
for (let i = 0; i < cells.length; i++) { | ||
if (i <= 13) { | ||
await expectAsync(cells[i].isInRange()).toBeResolvedTo(true); | ||
} else { | ||
await expectAsync(cells[i].isInRange()).toBeResolvedTo(false); | ||
} | ||
} | ||
}); | ||
|
||
it("should set the manually selected dates", async () => { | ||
const calendar = await loader.getHarness(MatCalendarHarness); | ||
const cells = await calendar.getCells(); | ||
await cells[7].select(); | ||
await cells[12].select(); | ||
|
||
const filterRange = dateFilter.getDateRange(); | ||
expect(filterRange.start).toEqual(new Date("2023-04-08")); | ||
expect(filterRange.end).toEqual(new Date("2023-04-13")); | ||
}); | ||
|
||
it("should set the dates selected via the preset options", async () => { | ||
component.selectRangeAndClose(0); | ||
|
||
const filterRange = dateFilter.getDateRange(); | ||
expect(filterRange.start).toEqual( | ||
moment("2023-04-08").startOf("day").toDate() | ||
); | ||
expect(filterRange.end).toEqual(moment("2023-04-08").endOf("day").toDate()); | ||
expect(dateFilter.selectedOption).toBe("0"); | ||
}); | ||
|
||
it("should highlight the date range when hovering over a option", async () => { | ||
const calendar = await loader.getHarness(MatCalendarHarness); | ||
const cells = await calendar.getCells(); | ||
component.preselectRange({ | ||
endOffsets: [{ amount: 1, unit: "months" }], | ||
label: "This and the coming month", | ||
}); | ||
const currentDayOfMonth = new Date().getDate(); | ||
for (let i = 0; i < cells.length; i++) { | ||
if (i < currentDayOfMonth - 1) { | ||
await expectAsync(cells[i].isInComparisonRange()).toBeResolvedTo(false); | ||
} else { | ||
await expectAsync(cells[i].isInComparisonRange()).toBeResolvedTo(true); | ||
} | ||
} | ||
}); | ||
|
||
it("should correctly calculate date ranges based on the config", () => { | ||
let res = calculateDateRange({ label: "Today" }); | ||
let fromDate = moment().startOf("day").toDate(); | ||
let toDate = moment().endOf("day").toDate(); | ||
expect(res).toEqual(new DateRange(fromDate, toDate)); | ||
|
||
let mockedToday = moment("2023-06-08").toDate(); | ||
jasmine.clock().mockDate(mockedToday); | ||
|
||
res = calculateDateRange({ | ||
startOffsets: [{ amount: 0, unit: "weeks" }], | ||
endOffsets: [{ amount: 0, unit: "weeks" }], | ||
label: "This week", | ||
}); | ||
fromDate = moment("2023-06-04").startOf("day").toDate(); | ||
toDate = moment("2023-06-10").endOf("day").toDate(); | ||
expect(res).toEqual(new DateRange(fromDate, toDate)); | ||
|
||
res = calculateDateRange({ | ||
startOffsets: [{ amount: 1, unit: "weeks" }], | ||
endOffsets: [{ amount: 1, unit: "weeks" }], | ||
label: "Next week", | ||
}); | ||
fromDate = moment("2023-06-11").startOf("day").toDate(); | ||
toDate = moment("2023-06-17").endOf("day").toDate(); | ||
expect(res).toEqual(new DateRange(fromDate, toDate)); | ||
|
||
res = calculateDateRange({ | ||
endOffsets: [ | ||
{ amount: -2, unit: "week" }, | ||
{ amount: 3, unit: "months" }, | ||
], | ||
label: | ||
"From today until endOf(today minus 2 weeks) and then endOf(this date plus 3 months)", | ||
}); | ||
fromDate = moment("2023-06-08").startOf("day").toDate(); | ||
toDate = moment("2023-08-26").endOf("day").toDate(); | ||
expect(res).toEqual(new DateRange(fromDate, toDate)); | ||
|
||
res = calculateDateRange({ | ||
endOffsets: [ | ||
{ amount: 3, unit: "months" }, | ||
{ amount: -2, unit: "week" }, | ||
], | ||
label: | ||
"From today until endOf(today minus 2 weeks) and then endOf(this date plus 3 months)", | ||
}); | ||
fromDate = moment("2023-06-08").startOf("day").toDate(); | ||
toDate = moment("2023-08-31").endOf("day").toDate(); | ||
expect(res).toEqual(new DateRange(fromDate, toDate)); | ||
}); | ||
}); |
121 changes: 121 additions & 0 deletions
121
...ore/filter/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { Component, Inject } from "@angular/core"; | ||
import { | ||
DateRange, | ||
MatDatepickerModule, | ||
MatDateSelectionModel, | ||
MatRangeDateSelectionModel, | ||
MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER, | ||
} from "@angular/material/datepicker"; | ||
import { | ||
MAT_DIALOG_DATA, | ||
MatDialogModule, | ||
MatDialogRef, | ||
} from "@angular/material/dialog"; | ||
import { MatButtonModule } from "@angular/material/button"; | ||
import { NgForOf } from "@angular/common"; | ||
import { DateRangeFilterConfigOption } from "app/core/entity-components/entity-list/EntityListConfig"; | ||
import moment from "moment"; | ||
import { FormsModule } from "@angular/forms"; | ||
import { DateFilter } from "../../filters/filters"; | ||
import { dateToString } from "../../../../utils/utils"; | ||
|
||
export const defaultDateFilters: DateRangeFilterConfigOption[] = [ | ||
{ | ||
label: $localize`:Filter label:Today`, | ||
}, | ||
{ | ||
startOffsets: [{ amount: 0, unit: "weeks" }], | ||
endOffsets: [{ amount: 0, unit: "weeks" }], | ||
label: $localize`:Filter label:This week`, | ||
}, | ||
{ | ||
startOffsets: [{ amount: -1, unit: "weeks" }], | ||
label: $localize`:Filter label:Since last week`, | ||
}, | ||
{ | ||
startOffsets: [{ amount: 0, unit: "months" }], | ||
endOffsets: [{ amount: 0, unit: "months" }], | ||
label: $localize`:Filter label:This month`, | ||
}, | ||
{ | ||
startOffsets: [{ amount: -1, unit: "months" }], | ||
endOffsets: [{ amount: -1, unit: "months" }], | ||
label: $localize`:Filter label:Last month`, | ||
}, | ||
]; | ||
|
||
@Component({ | ||
selector: "app-date-range-filter-panel", | ||
templateUrl: "./date-range-filter-panel.component.html", | ||
styleUrls: ["./date-range-filter-panel.component.scss"], | ||
providers: [ | ||
{ provide: MatDateSelectionModel, useClass: MatRangeDateSelectionModel }, | ||
MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER, | ||
], | ||
standalone: true, | ||
imports: [ | ||
MatDialogModule, | ||
MatButtonModule, | ||
MatDatepickerModule, | ||
NgForOf, | ||
FormsModule, | ||
], | ||
}) | ||
export class DateRangeFilterPanelComponent { | ||
selectedRangeValue = this.filter.getDateRange(); | ||
selectedOption = this.filter.getSelectedOption(); | ||
comparisonRange: DateRange<Date> = new DateRange(null, null); | ||
|
||
constructor( | ||
@Inject(MAT_DIALOG_DATA) public filter: DateFilter<any>, | ||
private dialogRef: MatDialogRef<DateRangeFilterPanelComponent> | ||
) {} | ||
|
||
preselectRange(dateRangeOption): void { | ||
this.comparisonRange = calculateDateRange(dateRangeOption); | ||
} | ||
|
||
unselectRange() { | ||
this.comparisonRange = new DateRange(null, null); | ||
} | ||
|
||
selectRangeAndClose(index: number): void { | ||
this.filter.selectedOption = index.toString(); | ||
this.dialogRef.close(); | ||
} | ||
|
||
selectedRangeChange(selectedDate: Date) { | ||
if (!this.selectedRangeValue?.start || this.selectedRangeValue?.end) { | ||
this.selectedRangeValue = new DateRange(selectedDate, null); | ||
} else { | ||
const start = this.selectedRangeValue.start; | ||
this.filter.selectedOption = | ||
start < selectedDate | ||
? dateToString(start) + "_" + dateToString(selectedDate) | ||
: dateToString(selectedDate) + "_" + dateToString(start); | ||
this.dialogRef.close(); | ||
} | ||
} | ||
} | ||
|
||
export function calculateDateRange( | ||
dateRangeOption: DateRangeFilterConfigOption | ||
): DateRange<Date> { | ||
const startOffsets = dateRangeOption.startOffsets ?? [ | ||
{ amount: 0, unit: "days" }, | ||
]; | ||
const endOffsets = dateRangeOption.endOffsets ?? [ | ||
{ amount: 0, unit: "days" }, | ||
]; | ||
|
||
const start = moment(); | ||
const end = moment(); | ||
|
||
startOffsets.forEach((offset) => start.add(offset.amount, offset.unit)); | ||
endOffsets.forEach((offset) => end.add(offset.amount, offset.unit)); | ||
|
||
start.startOf(startOffsets[0].unit); | ||
end.endOf(endOffsets[0].unit); | ||
|
||
return new DateRange(start.toDate(), end.toDate()); | ||
} |
25 changes: 25 additions & 0 deletions
25
src/app/core/filter/date-range-filter/date-range-filter.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<mat-form-field> | ||
<mat-label i18n="Input label">Enter a date range</mat-label> | ||
|
||
<mat-date-range-input> | ||
<input | ||
matStartDate | ||
(dateChange)="dateChangedManually()" | ||
[(ngModel)]="fromDate" | ||
i18n-placeholder="Date selection" | ||
placeholder="Start date" | ||
/> | ||
<input | ||
matEndDate | ||
(dateChange)="dateChangedManually()" | ||
[(ngModel)]="toDate" | ||
i18n-placeholder="Date selection" | ||
placeholder="End date" | ||
/> | ||
</mat-date-range-input> | ||
|
||
<mat-datepicker-toggle | ||
matSuffix | ||
(click)="openDialog($event)" | ||
></mat-datepicker-toggle> | ||
</mat-form-field> |
Empty file.
94 changes: 94 additions & 0 deletions
94
src/app/core/filter/date-range-filter/date-range-filter.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { ComponentFixture, TestBed } from "@angular/core/testing"; | ||
|
||
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 "../filters/filters"; | ||
import { defaultDateFilters } from "./date-range-filter-panel/date-range-filter-panel.component"; | ||
|
||
describe("DateRangeFilterComponent", () => { | ||
let component: DateRangeFilterComponent<any>; | ||
let fixture: ComponentFixture<DateRangeFilterComponent<any>>; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [MatNativeDateModule, NoopAnimationsModule], | ||
providers: [{ provide: MatDialog, useValue: null }], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(DateRangeFilterComponent); | ||
component = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it("should create", () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
|
||
it("should set the correct date filter when a new option is selected", () => { | ||
const dateFilter = new DateFilter("test", "Test", defaultDateFilters); | ||
|
||
dateFilter.selectedOption = "9"; | ||
component.filterConfig = dateFilter; | ||
expect(component.dateFilter.getFilter()).toBe(undefined); | ||
|
||
jasmine.clock().mockDate(new Date("2023-05-18")); | ||
dateFilter.selectedOption = "0"; | ||
component.filterConfig = dateFilter; | ||
let expectedDataFilter = { | ||
test: { | ||
$gte: "2023-05-18", | ||
$lte: "2023-05-18", | ||
}, | ||
}; | ||
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); | ||
|
||
dateFilter.selectedOption = "1"; | ||
component.filterConfig = dateFilter; | ||
expectedDataFilter = { | ||
test: { | ||
$gte: "2023-05-14", | ||
$lte: "2023-05-20", | ||
}, | ||
}; | ||
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); | ||
}); | ||
|
||
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"; | ||
component.filterConfig = dateFilter; | ||
expect(component.dateFilter.getFilter()).toBe(undefined); | ||
|
||
dateFilter.selectedOption = "2022-9-18_2023-01-3"; | ||
component.filterConfig = dateFilter; | ||
let expectedDataFilter = { | ||
test: { | ||
$gte: "2022-09-18", | ||
$lte: "2023-01-03", | ||
}, | ||
}; | ||
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); | ||
}); | ||
|
||
it("should set the correct date filter when changing the date range manually", () => { | ||
component.filterConfig = new DateFilter("test", "test", []); | ||
component.fromDate = new Date("2021-10-28"); | ||
component.toDate = new Date("2024-02-12"); | ||
|
||
component.dateChangedManually(); | ||
|
||
expect(component.dateFilter.selectedOption).toEqual( | ||
"2021-10-28_2024-02-12" | ||
); | ||
let expectedDataFilter = { | ||
test: { | ||
$gte: "2021-10-28", | ||
$lte: "2024-02-12", | ||
}, | ||
}; | ||
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter); | ||
}); | ||
}); |
60 changes: 60 additions & 0 deletions
60
src/app/core/filter/date-range-filter/date-range-filter.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { Component, EventEmitter, Input, Output } from "@angular/core"; | ||
import { MatDialog } from "@angular/material/dialog"; | ||
import { Entity } from "app/core/entity/model/entity"; | ||
import { DateFilter, Filter } from "../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 "app/utils/utils"; | ||
|
||
@Component({ | ||
selector: "app-date-range-filter", | ||
templateUrl: "./date-range-filter.component.html", | ||
styleUrls: ["./date-range-filter.component.scss"], | ||
standalone: true, | ||
imports: [MatFormFieldModule, MatDatepickerModule, FormsModule], | ||
}) | ||
export class DateRangeFilterComponent<T extends Entity> { | ||
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(); | ||
} | ||
|
||
constructor(private dialog: MatDialog) {} | ||
|
||
private initDates() { | ||
const range = this.dateFilter.getDateRange(); | ||
if (range.start !== this.fromDate && range.end !== this.toDate) { | ||
this.fromDate = range.start; | ||
this.toDate = range.end; | ||
this.selectedOptionChange.emit(this.dateFilter.selectedOption); | ||
} | ||
} | ||
|
||
dateChangedManually() { | ||
if (isValidDate(this.fromDate) && isValidDate(this.toDate)) { | ||
this.dateFilter.selectedOption = | ||
dateToString(this.fromDate) + "_" + dateToString(this.toDate); | ||
} | ||
this.selectedOptionChange.emit(this.dateFilter.selectedOption); | ||
} | ||
|
||
openDialog(e: Event) { | ||
e.stopPropagation(); | ||
this.dialog | ||
.open(DateRangeFilterPanelComponent, { | ||
width: "600px", | ||
minWidth: "400px", | ||
data: this.dateFilter, | ||
}) | ||
.afterClosed() | ||
.subscribe(() => this.initDates()); | ||
} | ||
} |
127 changes: 0 additions & 127 deletions
127
src/app/core/filter/filter-selection/filter-selection.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,20 @@ | ||
<app-list-filter | ||
*ngFor="let filter of filterSelections" | ||
[filterConfig]="filter.filterSettings" | ||
[(selectedOption)]="filter.selectedOption" | ||
(selectedOptionChange)="filterOptionSelected(filter, $event)" | ||
angulartics2On="click" | ||
[angularticsCategory]="entityType?.ENTITY_TYPE" | ||
[angularticsAction]="'filter_' + urlPath" | ||
[angularticsLabel]="filter.filterSettings.label" | ||
> | ||
</app-list-filter> | ||
<ng-container *ngFor="let filter of filterSelections"> | ||
<app-list-filter | ||
*ngIf="filter.name !== 'date'" | ||
[filterConfig]="filter" | ||
[(selectedOption)]="filter.selectedOption" | ||
(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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 16 additions & 10 deletions
26
...filter-selection/filter-selection.spec.ts → src/app/core/filter/filters/filters.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,38 @@ | ||
import { FilterSelection } from "./filter-selection"; | ||
import { SelectableFilter } from "./filters"; | ||
import { FilterService } from "../filter.service"; | ||
|
||
describe("FilterSelection", () => { | ||
describe("Filters", () => { | ||
const filterService = new FilterService(undefined); | ||
it("create an instance", () => { | ||
const fs = new FilterSelection("", []); | ||
const fs = new SelectableFilter( | ||
"", | ||
[{ key: "", label: "", filter: "" }], | ||
"" | ||
); | ||
expect(fs).toBeTruthy(); | ||
}); | ||
|
||
it("init new options", () => { | ||
const fs = new FilterSelection("", []); | ||
const fs = new SelectableFilter( | ||
"", | ||
[{ key: "", label: "", filter: "" }], | ||
"" | ||
); | ||
|
||
const keys = ["x", "y"]; | ||
fs.options = FilterSelection.generateOptions(keys, "category"); | ||
fs.options = SelectableFilter.generateOptions(keys, "category"); | ||
|
||
expect(fs.options).toHaveSize(keys.length + 1); | ||
|
||
const testData = [ | ||
{ id: 1, category: "x" }, | ||
{ id: 2, category: "y" }, | ||
] as any; | ||
const selectedCategory = "x"; | ||
const predicate = filterService.getFilterPredicate( | ||
fs.getFilter(selectedCategory) | ||
); | ||
fs.selectedOption = "x"; | ||
const predicate = filterService.getFilterPredicate(fs.getFilter()); | ||
const filteredData = testData.filter(predicate); | ||
|
||
expect(filteredData).toHaveSize(1); | ||
expect(filteredData[0].category).toBe(selectedCategory); | ||
expect(filteredData[0].category).toBe("x"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
/* | ||
* This file is part of ndb-core. | ||
* | ||
* ndb-core is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* ndb-core is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
import { ConfigurableEnumValue } from "app/core/configurable-enum/configurable-enum.interface"; | ||
import { | ||
BooleanFilterConfig, | ||
DateRangeFilterConfigOption, | ||
} from "app/core/entity-components/entity-list/EntityListConfig"; | ||
import { DataFilter } from "../../entity-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 "../date-range-filter/date-range-filter-panel/date-range-filter-panel.component"; | ||
import moment from "moment/moment"; | ||
|
||
export abstract class Filter<T extends Entity> { | ||
public selectedOption: string; | ||
|
||
constructor(public name: string, public label: string = name) {} | ||
|
||
abstract getFilter(): DataFilter<T>; | ||
} | ||
|
||
/** | ||
* 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> { | ||
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<Date> { | ||
if (this.getSelectedOption()) { | ||
return calculateDateRange(this.getSelectedOption()); | ||
} | ||
const dates = this.selectedOption?.split("_"); | ||
if (dates?.length == 2) { | ||
const firstDate = new Date(dates[0]); | ||
const secondDate = new Date(dates[1]); | ||
if (isValidDate(firstDate) && isValidDate(secondDate)) { | ||
return new DateRange(firstDate, secondDate); | ||
} | ||
} | ||
return new DateRange(undefined, undefined); | ||
} | ||
|
||
getFilter(): DataFilter<T> { | ||
const range = this.getDateRange(); | ||
if (range.start && range.end) { | ||
return { | ||
[this.name]: { | ||
$gte: moment(range.start).format("YYYY-MM-DD"), | ||
$lte: moment(range.end).format("YYYY-MM-DD"), | ||
}, | ||
} as DataFilter<T>; | ||
} | ||
} | ||
|
||
getSelectedOption() { | ||
return this.rangeOptions[this.selectedOption as any]; | ||
} | ||
} | ||
|
||
/** | ||
* Generic configuration for a filter with different selectable {@link FilterSelectionOption} options. | ||
* | ||
* This is a reusable format for any kind of dropdown or selection component that offers the user a choice | ||
* to narrow down a list of data items. | ||
* As the filter function is provided as part of each {@link FilterSelectionOption} | ||
* an instance of this FilterSelection class can manage all filter selection logic. | ||
*/ | ||
export class SelectableFilter<T extends Entity> extends Filter<T> { | ||
/** | ||
* Generate filter options dynamically from the given value to be matched. | ||
* | ||
* This is a utility function to make it easier to generate {@link FilterSelectionOption}s for standard cases | ||
* if you simply want each option to filter items having the given attribute matching different values. | ||
* If you have more sophisticated filtering needs, use the constructor to set {@link FilterSelectionOption}s that | ||
* you created yourself. | ||
* | ||
* @param valuesToMatchAsOptions An array of values to be matched. | ||
* A separate FilterSelectionOption is created for each value with a filter | ||
* that is true of a data item's property exactly matches that value. | ||
* @param attributeName The name of the property of a data item that is compared to the value in the filter function. | ||
*/ | ||
public static generateOptions<T extends Entity>( | ||
valuesToMatchAsOptions: string[], | ||
attributeName: string | ||
): FilterSelectionOption<T>[] { | ||
const options = [ | ||
{ | ||
key: "", | ||
label: $localize`:generic filter option showing all entries:All`, | ||
filter: {} as DataFilter<T>, | ||
}, | ||
]; | ||
|
||
options.push( | ||
...valuesToMatchAsOptions | ||
.filter((k) => !!k) | ||
.map((k) => ({ | ||
key: k.toLowerCase(), | ||
label: k.toString(), | ||
filter: { [attributeName]: k } as DataFilter<T>, | ||
})) | ||
); | ||
|
||
return options; | ||
} | ||
|
||
/** | ||
* Create a FilterSelection with different options to be selected. | ||
* @param name The name or id describing this filter | ||
* @param options An array of different filtering variants to chose between | ||
* @param label The user-friendly label describing this filter-selection | ||
* (optional, defaults to the name of the selection) | ||
*/ | ||
constructor( | ||
public name: string, | ||
public options: FilterSelectionOption<T>[], | ||
public label: string = name | ||
) { | ||
super(name, label); | ||
this.selectedOption = this.options[0]?.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<T> { | ||
return this.options.find((option) => 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<T> { | ||
const option = this.getOption(this.selectedOption); | ||
|
||
if (!option) { | ||
return this.defaultFilter as DataFilter<T>; | ||
} else { | ||
return option.filter; | ||
} | ||
} | ||
} | ||
|
||
export class BooleanFilter<T extends Entity> extends SelectableFilter<T> { | ||
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: { [config.id]: false }, | ||
}, | ||
], | ||
label | ||
); | ||
} | ||
} | ||
|
||
export class ConfigurableEnumFilter< | ||
T extends Entity | ||
> extends SelectableFilter<T> { | ||
constructor( | ||
name: string, | ||
label: string, | ||
enumValues: ConfigurableEnumValue[] | ||
) { | ||
let options: FilterSelectionOption<T>[] = [ | ||
{ | ||
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<T extends Entity> extends SelectableFilter<T> { | ||
constructor(name: string, label: string, filterEntities) { | ||
filterEntities.sort((a, b) => a.toString().localeCompare(b.toString())); | ||
const options: FilterSelectionOption<T>[] = [ | ||
{ | ||
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); | ||
} | ||
} | ||
|
||
/** | ||
* Represents one specific option to filter data in a certain way. | ||
* used by {@link SelectableFilter} | ||
*/ | ||
export interface FilterSelectionOption<T> { | ||
/** identifier for this option in the parent FilterSelection instance */ | ||
key: string; | ||
|
||
/** label displayed for this option to the user in the UI */ | ||
label: string; | ||
|
||
/** Optional color */ | ||
color?: string; | ||
|
||
/** | ||
* The filter query which should be used if this filter is selected | ||
*/ | ||
filter: DataFilter<T> | any; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters