diff --git a/src/app/applications/index.component.ts b/src/app/applications/index.component.ts index b1e279a9c..ca33c66a1 100644 --- a/src/app/applications/index.component.ts +++ b/src/app/applications/index.component.ts @@ -8,8 +8,8 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { DashboardIndexService } from '@app/dashboard/services/dashboard-index.service'; import { DashboardStorageService } from '@app/dashboard/services/dashboard-storage.service'; import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { TableHandler } from '@app/types/components'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { TableType } from '@app/types/table'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { PageHeaderComponent } from '@components/page-header.component'; @@ -52,7 +52,7 @@ import { ApplicationRaw } from './types'; TasksStatusesService, ApplicationsFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: ApplicationsFiltersService }, DashboardIndexService, diff --git a/src/app/applications/services/applications-filters.service.ts b/src/app/applications/services/applications-filters.service.ts index 57370e7e5..7e410a081 100644 --- a/src/app/applications/services/applications-filters.service.ts +++ b/src/app/applications/services/applications-filters.service.ts @@ -2,13 +2,13 @@ import { ApplicationRawEnumField } from '@aneoconsultingfr/armonik.api.angular'; import { Injectable } from '@angular/core'; import { Scope } from '@app/types/config'; import { FilterFor } from '@app/types/filter-definition'; -import { AbstractFilterService } from '@app/types/services/filtersService'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { ApplicationFilterField, ApplicationRawFilters, ApplicationsFiltersDefinition } from '../types'; @Injectable({ providedIn: 'root' }) -export class ApplicationsFiltersService extends AbstractFilterService<ApplicationRawEnumField> { +export class ApplicationsFiltersService extends DataFilterService<ApplicationRawEnumField> { protected readonly scope: Scope = 'applications'; readonly rootField: Record<ApplicationRawEnumField, string> = { [ApplicationRawEnumField.APPLICATION_RAW_ENUM_FIELD_NAME]: $localize`Name`, diff --git a/src/app/components/filters/filters-chips.component.html b/src/app/components/filters/filters-chips.component.html index cc420d9f7..496cde819 100644 --- a/src/app/components/filters/filters-chips.component.html +++ b/src/app/components/filters/filters-chips.component.html @@ -1,11 +1,7 @@ <mat-chip-listbox> - @for(filter of filtersAnd; track filter.field) { + @for(filter of filters(); track $index) { <mat-chip class="mat-mdc-standard-chip mat-primary mat-mdc-chip-selected"> - @if(filter.field) { - <span>{{ content(filter) }}</span> - } @else { - <span i18n>No field</span> - } + <span>{{ filter }}</span> </mat-chip> @if(!$last) { <span class="text" i18n>AND</span> diff --git a/src/app/components/filters/filters-chips.component.spec.ts b/src/app/components/filters/filters-chips.component.spec.ts index 1d9a69a88..3ef03eb3f 100644 --- a/src/app/components/filters/filters-chips.component.spec.ts +++ b/src/app/components/filters/filters-chips.component.spec.ts @@ -1,28 +1,32 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; +import { FilterDateOperator, FilterDurationOperator, FilterStatusOperator, FilterStringOperator, TaskOptionEnumField, TaskSummaryEnumField } from '@aneoconsultingfr/armonik.api.angular'; +import { TestBed } from '@angular/core/testing'; import { FilterDefinition } from '@app/types/filter-definition'; -import { Filter } from '@app/types/filters'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersService } from '@services/filters.service'; import { UtilsService } from '@services/utils.service'; import { FiltersChipsComponent } from './filters-chips.component'; describe('FiltersChipsComponent', () => { let component: FiltersChipsComponent<number, number | null>; - let fixture: ComponentFixture<FiltersChipsComponent<number, number | null>>; - const mockDataFilterService = { - retrieveFiltersDefinitions: jest.fn(() => { - return filterDefinitions; - }), - retrieveLabel: jest.fn() - }; - const filterDefinitions: FilterDefinition<number, number>[] = [ + + const labels = { + [TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_TASK_ID]: 'TaskId', + [TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_STATUS]: 'Status', + [TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_CREATED_AT]: 'CreatedAt', + } as Record<TaskSummaryEnumField, string>; + + const optionsLabels = { + [TaskOptionEnumField.TASK_OPTION_ENUM_FIELD_MAX_DURATION]: 'Max Duration', + } as Record<TaskOptionEnumField, string>; + + const filtersDefinitions: FilterDefinition<TaskSummaryEnumField, TaskOptionEnumField>[] = [ { - field: 4, - type: 'number', + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_TASK_ID, + type: 'string', for: 'root' }, { - field: 1, + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_STATUS, type: 'status', for: 'root', statuses: [ @@ -31,127 +35,134 @@ describe('FiltersChipsComponent', () => { ] }, { - field: 2, - type: 'string', + field: TaskOptionEnumField.TASK_OPTION_ENUM_FIELD_MAX_DURATION, + type: 'duration', for: 'options' }, { - field: 3, - type: 'array', - for: 'root' - }, - { - field: 6, + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_CREATED_AT, type: 'date', for: 'root' - }, - ({ - field: 5, - type: 'unknownType', - for: 'root' - } as unknown as FilterDefinition<number, number>), - { - field: 1, - type: 'duration', - for: 'options' } ]; - beforeEach(async () => { - await TestBed.configureTestingModule({ + const mockDataFilterService = { + filtersDefinitions: filtersDefinitions, + retrieveLabel: jest.fn((for_: string, field: TaskSummaryEnumField | TaskOptionEnumField) => { + if (for_ === 'root') { + return labels[field as TaskSummaryEnumField]; + } else { + return optionsLabels[field as TaskOptionEnumField]; + } + }) + }; + + beforeAll(() => { + component = TestBed.configureTestingModule({ providers: [ FiltersChipsComponent, FiltersService, UtilsService, - { provide: DATA_FILTERS_SERVICE, useValue: mockDataFilterService } + { provide: DataFilterService, useValue: mockDataFilterService } ] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(FiltersChipsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - - mockDataFilterService.retrieveLabel.mockImplementation((for_: string, field: number) => { - if (for_ === 'root' && field === 4) return 'number'; - else if (for_ === 'root' && field === 1) return 'status'; - else if (for_ === 'root' && field === 6) return 'date'; - else return 'other'; - }); + }).inject(FiltersChipsComponent); }); it('should create', () => { expect(component).toBeTruthy(); }); - it('should create the content of the component', () => { - const filter: Filter<number, number> = { - field: 4, - for: 'root', - operator: 1, - value: 2 - }; - expect(component.content(filter)).toEqual('number Not Equal 2'); - }); - - it('should create the content of the component with statuses', () => { - const filter: Filter<number, number> = { - field: 1, - for: 'root', - operator: 0, - value: 'status1' - }; - expect(component.content(filter)).toEqual('status Equal dispatched'); - }); - - it('should create the label of the date filter', () => { - const filter: Filter<number, number> = { - field: 6, - for: 'root', - operator: 0, - value: 0 - }; - expect(component.content(filter)).toEqual('date Equal Thu, 01 Jan 1970 00:00:00 GMT'); + describe('Filters cases', () => { + it('should create the label of a filter', () => { + component.filtersAnd = [{ + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_TASK_ID, + for: 'root', + operator: FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, + value: '0', + }]; + expect(component.filters()).toEqual(['TaskId Equal 0']); + }); + + it('should create the label of a status filter', () => { + component.filtersAnd = [{ + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_STATUS, + for: 'root', + operator: FilterStatusOperator.FILTER_STATUS_OPERATOR_NOT_EQUAL, + value: 'status1', + }]; + expect(component.filters()).toEqual(['Status Not Equal dispatched']); + }); + + it('should create the label of a date filter', () => { + component.filtersAnd = [{ + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_CREATED_AT, + for: 'root', + operator: FilterDateOperator.FILTER_DATE_OPERATOR_AFTER_OR_EQUAL, + value: '0', + }]; + expect(component.filters()).toEqual(['CreatedAt After or Equal Thu, 01 Jan 1970 00:00:00 GMT']); + }); + + it('should create the label of a duration filter', () => { + component.filtersAnd = [{ + field: TaskOptionEnumField.TASK_OPTION_ENUM_FIELD_MAX_DURATION, + for: 'options', + operator: FilterDurationOperator.FILTER_DURATION_OPERATOR_LONGER_THAN_OR_EQUAL, + value: '94350', + }]; + expect(component.filters()).toEqual(['Max Duration Longer or Equal 26h 12m 30s']); + }); + + it('should create the label of a custom filter', () => { + component.filtersAnd = [{ + field: 'CustomField', + for: 'custom', + operator: FilterStringOperator.FILTER_STRING_OPERATOR_CONTAINS, + value: 'a' + }]; + expect(component.filters()).toEqual(['CustomField Contains a']); + }); }); - it('should create the content of the component even with no for', () => { - const filter: Filter<number, number> = { - field: 1, - for: null, - operator: 0, - value: 'status1' - }; - expect(component.content(filter)).toEqual('status Equal dispatched'); - }); + describe('Error cases', () => { + it('should create the label if there is no field', () => { + component.filtersAnd = [{ + field: null, + for: 'root', + operator: FilterDateOperator.FILTER_DATE_OPERATOR_BEFORE_OR_EQUAL, + value: 'abc' + }]; + expect(component.filters()).toEqual(['No field']); + }); - it('should create the content of the component with a custom filter', () => { - const filter: Filter<number, number> = { - field: 'test', - operator: 0, - for: 'custom', - value: 'True' - }; - expect(component.content(filter)).toEqual('test Equal True'); - }); + it('should create the label if there is no operator', () => { + component.filtersAnd = [{ + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_TASK_ID, + for: 'root', + operator: null, + value: 'a' + }]; + expect(component.filters()).toEqual(['No operator']); + }); - it('should not create the content if there is no values', () => { - const filter: Filter<number, number> = { - field: 2, - for: 'options', - operator: 1, - value: null - }; - expect(component.content(filter)).toEqual('other has no value'); - }); + it('should create the label if there is no value', () => { + component.filtersAnd = [{ + field: TaskOptionEnumField.TASK_OPTION_ENUM_FIELD_MAX_DURATION, + for: 'options', + operator: FilterDurationOperator.FILTER_DURATION_OPERATOR_EQUAL, + value: null + }]; + expect(component.filters()).toEqual(['Max Duration has no value']); + }); - it('should show a duration appropriately', () => { - const filter: Filter<number, number> = { - field: 1, - for: 'options', - operator: 1, - value: 94350 - }; - expect(component.content(filter)).toEqual('other Not Equal 26h 12m 30s'); + it('should create the label if the field is invalid', () => { + component.filtersAnd = [{ + field: TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_FETCHED_AT, + for: 'root', + operator: FilterDateOperator.FILTER_DATE_OPERATOR_BEFORE_OR_EQUAL, + value: '12349000' + }]; + expect(component.filters()).toEqual(['Invalid Filter Field']); + }); }); }); \ No newline at end of file diff --git a/src/app/components/filters/filters-chips.component.ts b/src/app/components/filters/filters-chips.component.ts index 7a159924e..a61f54190 100644 --- a/src/app/components/filters/filters-chips.component.ts +++ b/src/app/components/filters/filters-chips.component.ts @@ -1,7 +1,7 @@ -import { Component, Input, inject } from '@angular/core'; +import { Component, Input, inject, signal } from '@angular/core'; import { MatChipsModule } from '@angular/material/chips'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; -import { Filter, FiltersAnd } from '@app/types/filters'; +import { Filter, FiltersAnd, FiltersEnums, FiltersOptionsEnums } from '@app/types/filters'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersService } from '@services/filters.service'; import { UtilsService } from '@services/utils.service'; @@ -27,49 +27,55 @@ import { UtilsService } from '@services/utils.service'; FiltersService, ], }) -export class FiltersChipsComponent<T extends number, U extends number | null = null> { +export class FiltersChipsComponent<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> { private readonly filtersService = inject(FiltersService); - private readonly utilsService = inject(UtilsService<T, U>); - private readonly dataFiltersService = inject(DATA_FILTERS_SERVICE); + private readonly utilsService = inject(UtilsService<F, O>); + private readonly dataFiltersService = inject(DataFilterService); - @Input({ required: true }) filtersAnd: FiltersAnd<T, U> = []; + readonly filters = signal<string[]>([]); - content(filter: Filter<T, U>): string { + @Input({ required: true }) set filtersAnd(entry: FiltersAnd<F, O>) { + this.filters.set(entry.map(filter => this.toContent(filter))); + } - if(!filter.for) { - filter.for = 'root'; - } - - let label: string; - if (filter.for !== 'custom') { - label = this.dataFiltersService.retrieveLabel(filter.for, Number(filter.field)); - } else { - label = (filter.field as string); - } + private toContent(filter: Filter<F, O>): string { + if (filter.field !== null && filter.field !== undefined) { + const label = filter.for !== 'custom' ? this.dataFiltersService.retrieveLabel(filter.for as 'root' | 'options', Number(filter.field)) : (filter.field as string); - if (filter.value === null) - return label + ' ' + $localize`has no value`; + if (filter.value === null) { + return label + ' ' + $localize`has no value`; + } else if (filter.operator === null) { + return $localize`No operator`; + } - const filtersDefinitions = this.dataFiltersService.retrieveFiltersDefinitions(); - const type = this.utilsService.recoverType(filter, filtersDefinitions); - const operator = this.filtersService.findOperators(type)[filter.operator as number]; + try { + const type = this.utilsService.recoverType(filter, this.dataFiltersService.filtersDefinitions); + const operator = this.filtersService.findOperators(type)[filter.operator]; - if (type === 'status') { - const statuses = this.utilsService.recoverStatuses(filter, filtersDefinitions); - const status = statuses.find(status => status.key.toString() === filter.value?.toString()); - return `${label} ${operator} ${status?.value}`; + switch (type) { + case 'status': { + const statuses = this.utilsService.recoverStatuses(filter, this.dataFiltersService.filtersDefinitions); + const status = statuses.find(status => status.key.toString() === filter.value?.toString()); + return `${label} ${operator} ${status?.value}`; + } + case 'date': { + return `${label} ${operator} ${new Date(Number(filter.value) * 1000).toUTCString()}`; + } + case 'duration': { + return `${label} ${operator} ${this.durationToString(Number(filter.value))}`; + } + default: { + return `${label} ${operator} ${filter.value}`; + } + } + } catch { + return $localize`Invalid Filter Field`; + } } - else if (type === 'date') { - return `${label} ${operator} ${new Date(Number(filter.value) * 1000).toUTCString()}`; - } - else if (type === 'duration') { - return `${label} ${operator} ${this.durationToString(Number(filter.value))}`; - } - - return `${label} ${operator} ${filter.value}`; + return $localize`No field`; } - durationToString(value: number): string { + private durationToString(value: number): string { let resultString = ''; const hours = Math.floor(Number(value)/3600); const minutes = Math.floor((Number(value)%3600)/60); diff --git a/src/app/components/filters/filters-dialog-and.component.html b/src/app/components/filters/filters-dialog-and.component.html index 1138cfdc3..956e3b27e 100644 --- a/src/app/components/filters/filters-dialog-and.component.html +++ b/src/app/components/filters/filters-dialog-and.component.html @@ -1,10 +1,5 @@ -<app-filters-dialog-filter-field - [first]="first" - [filter]="filter" - [customColumns]="customColumns" - > -</app-filters-dialog-filter-field> +<app-filters-dialog-filter-field [first]="first" [filter]="filter" [customColumns]="customColumns" /> <button mat-icon-button (click)="onRemove()" matTooltip="Remove filter"> - <mat-icon aria-hidden="true" [fontIcon]="getIcon('delete')"></mat-icon> + <mat-icon aria-hidden="true" [fontIcon]="getIcon('delete')"/> </button> diff --git a/src/app/components/filters/filters-dialog-and.component.spec.ts b/src/app/components/filters/filters-dialog-and.component.spec.ts index 6d6755657..6b9101e37 100644 --- a/src/app/components/filters/filters-dialog-and.component.spec.ts +++ b/src/app/components/filters/filters-dialog-and.component.spec.ts @@ -1,6 +1,5 @@ import { TestBed } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { IconsService } from '@services/icons.service'; import { FiltersDialogAndComponent } from './filters-dialog-and.component'; @@ -14,12 +13,6 @@ describe('FiltersDialogAndComponent', () => { providers: [ FiltersDialogAndComponent, IconsService, - { provide: DATA_FILTERS_SERVICE, useValue: { - retrieveFiltersDefinitions: jest.fn(() => { - return []; - }), - retrieveLabel: jest.fn(), - }} ] }).inject(FiltersDialogAndComponent<number, number>); component.filter = { diff --git a/src/app/components/filters/filters-dialog-and.component.ts b/src/app/components/filters/filters-dialog-and.component.ts index bdc96b166..f8cef1058 100644 --- a/src/app/components/filters/filters-dialog-and.component.ts +++ b/src/app/components/filters/filters-dialog-and.component.ts @@ -3,7 +3,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { CustomColumn } from '@app/types/data'; -import { Filter } from '@app/types/filters'; +import { Filter, FiltersEnums, FiltersOptionsEnums } from '@app/types/filters'; import { IconsService } from '@services/icons.service'; import { FiltersDialogFilterFieldComponent } from './filters-dialog-filter-field.component'; @@ -30,12 +30,12 @@ import { FiltersDialogFilterFieldComponent } from './filters-dialog-filter-field IconsService, ], }) -export class FiltersDialogAndComponent<T extends number, U extends number | null = null> { +export class FiltersDialogAndComponent<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> { @Input({ required: true }) first: boolean; - @Input({ required: true }) filter: Filter<T, U>; + @Input({ required: true }) filter: Filter<F, O>; @Input() customColumns: CustomColumn[]; - @Output() removeChange: EventEmitter<Filter<T, U>> = new EventEmitter<Filter<T, U>>(); + @Output() removeChange: EventEmitter<Filter<F, O>> = new EventEmitter<Filter<F, O>>(); private readonly iconsService = inject(IconsService); diff --git a/src/app/components/filters/filters-dialog-filter-field.component.spec.ts b/src/app/components/filters/filters-dialog-filter-field.component.spec.ts index f4ed2c527..07aac6006 100644 --- a/src/app/components/filters/filters-dialog-filter-field.component.spec.ts +++ b/src/app/components/filters/filters-dialog-filter-field.component.spec.ts @@ -1,31 +1,15 @@ import { TestBed } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { CustomColumn } from '@app/types/data'; import { FilterDefinition } from '@app/types/filter-definition'; import { Filter, FilterType } from '@app/types/filters'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersService } from '@services/filters.service'; import { FiltersDialogFilterFieldComponent } from './filters-dialog-filter-field.component'; describe('FiltersDialogFilterFieldComponent', () => { let component: FiltersDialogFilterFieldComponent<number, number>; - const mockDataFiltersService = { - retrieveFiltersDefinitions: jest.fn(() => { - return filterDefinitions; - }), - retrieveLabel: jest.fn((for_: string, value: number) => { - return propertiesLabel[value]; - }), - retrieveField: jest.fn((value: string) => { - const values = Object.values(propertiesLabel); - const index = values.findIndex(label => label.toLowerCase() === value.toLowerCase()); - const key = index !== -1 ? Number(Object.keys(propertiesLabel).at(index)) : index; - const for_ = filterDefinitions.find(def => def.field === key)?.for ?? -1; - return { for: for_ ?? 'custom', index: key }; - }) - }; - const allStatuses = [ { key: 0, value: 'Creation' }, { key: 1, value: 'Submitted' }, @@ -43,7 +27,7 @@ describe('FiltersDialogFilterFieldComponent', () => { 8: 'Client Submission' }; - const filterDefinitions: FilterDefinition<number, number>[] = [ + const filtersDefinitions: FilterDefinition<number, number>[] = [ { field: 4, type: 'number', @@ -96,13 +80,27 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'someValue' }; + const mockDataFiltersService = { + filtersDefinitions: filtersDefinitions, + retrieveLabel: jest.fn((for_: string, value: number) => { + return propertiesLabel[value]; + }), + retrieveField: jest.fn((value: string) => { + const values = Object.values(propertiesLabel); + const index = values.findIndex(label => label.toLowerCase() === value.toLowerCase()); + const key = index !== -1 ? Number(Object.keys(propertiesLabel).at(index)) : index; + const for_ = filtersDefinitions.find(def => def.field === key)?.for ?? -1; + return { for: for_ ?? 'custom', index: key }; + }) + }; + beforeEach(async () => { component = TestBed.configureTestingModule({ imports: [BrowserAnimationsModule], providers: [ FiltersDialogFilterFieldComponent, FiltersService, - { provide: DATA_FILTERS_SERVICE, useValue: mockDataFiltersService } + { provide: DataFilterService, useValue: mockDataFiltersService } ] }).inject(FiltersDialogFilterFieldComponent); @@ -148,10 +146,6 @@ describe('FiltersDialogFilterFieldComponent', () => { }); }); - it('should retrieve the filter definitions', () => { - expect(component.filtersDefinitions).toEqual(filterDefinitions); - }); - describe('onPropertyChange', () => { it('should update filter', () => { component.onPropertyChange('Created at'); @@ -185,11 +179,11 @@ describe('FiltersDialogFilterFieldComponent', () => { }); it('should retrieve the key of the status', () => { - expect(component.retrieveStatusKey(allStatuses[1].value)).toEqual(1); + expect(component['retrieveStatusKey'](allStatuses[1].value)).toEqual(1); }); it('should return null if the label does not exists', () => { - expect(component.retrieveStatusKey('Unexisting')).toEqual(null); + expect(component['retrieveStatusKey']('Unexisting')).toEqual(null); }); }); @@ -275,7 +269,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'someValue' }; component.filter = filter; - expect(component.findType()).toEqual('number'); + expect(component['findType']()).toEqual('number'); }); it('should return type string if the field is null', () => { @@ -286,7 +280,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'someValue' }; component.filter = filter; - expect(component.findType()).toEqual('string'); + expect(component['findType']()).toEqual('string'); }); it('should return type string if the definition does not exists', () => { @@ -297,7 +291,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'someValue' }; component.filter = filter; - expect(component.findType()).toEqual('string'); + expect(component['findType']()).toEqual('string'); }); }); @@ -310,7 +304,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'myStatus' }; component.filter = statusFilter; - expect(component.findStatuses()).toEqual(allStatuses); + expect(component['findStatuses']()).toEqual(allStatuses); }); it('should return an empty status list if the filter has no field', () => { @@ -321,7 +315,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'myStatus' }; component.filter = statusFilter; - expect(component.findStatuses()).toEqual([]); + expect(component['findStatuses']()).toEqual([]); }); it('should return an empty status list if the filter definition does not exists', () => { @@ -332,7 +326,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'myStatus' }; component.filter = statusFilter; - expect(component.findStatuses()).toEqual([]); + expect(component['findStatuses']()).toEqual([]); }); it('should return an empty status list if the filter is not of type status', () => { @@ -343,7 +337,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'someValue' }; component.filter = notAStatusFilter; - expect(component.findStatuses()).toEqual([]); + expect(component['findStatuses']()).toEqual([]); }); }); @@ -356,7 +350,7 @@ describe('FiltersDialogFilterFieldComponent', () => { value: 'myStatus' }; component.filter = filter; - expect(component.findOperators()).toEqual(new FiltersService()['filterNumberOperators']); + expect(component['findOperators']()).toEqual(new FiltersService()['filterNumberOperators']); }); }); @@ -374,7 +368,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should handle strings', () => { component.type = 'string'; - component.setInput(); + component['setInput'](); expect(component.input).toEqual({ type: 'string', value: filter.value @@ -383,7 +377,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should handle numbers', () => { component.type = 'number'; - component.setInput(); + component['setInput'](); expect(component.input).toEqual({ type: 'number', value: Number(filter.value) @@ -392,7 +386,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should handle arrays', () => { component.type = 'array'; - component.setInput(); + component['setInput'](); expect(component.input).toEqual({ type: 'array', value: filter.value @@ -402,7 +396,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should handle statuses', () => { component.allStatuses = allStatuses; component.type = 'status'; - component.setInput(); + component['setInput'](); expect(component.input).toEqual({ type: 'status', value: 'Submitted' @@ -411,7 +405,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should handle dates', () => { component.type = 'date'; - component.setInput(); + component['setInput'](); expect(component.input).toEqual({ type: 'date', value: new Date(Number(filter.value) * 1000) @@ -420,7 +414,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should handle durations', () => { component.type = 'duration'; - component.setInput(); + component['setInput'](); expect(component.input).toEqual({ type: 'duration', value: filter.value @@ -429,7 +423,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should handle booleans', () => { component.type = 'boolean'; - component.setInput(); + component['setInput'](); expect(component.input).toEqual({ type: 'boolean', value: filter.value @@ -439,7 +433,7 @@ describe('FiltersDialogFilterFieldComponent', () => { it('should throw an error in case of unknown type', () => { const type = 'unknown' as FilterType; component.type = type; - expect(() => component.setInput()).toThrow(`Unknown type ${type}`); + expect(() => component['setInput']()).toThrow(`Unknown type ${type}`); }); }); }); \ No newline at end of file diff --git a/src/app/components/filters/filters-dialog-filter-field.component.ts b/src/app/components/filters/filters-dialog-filter-field.component.ts index bf681260a..1ca24b69e 100644 --- a/src/app/components/filters/filters-dialog-filter-field.component.ts +++ b/src/app/components/filters/filters-dialog-filter-field.component.ts @@ -1,13 +1,12 @@ -import { AsyncPipe, KeyValuePipe } from '@angular/common'; import { Component, Input, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { CustomColumn } from '@app/types/data'; import { FilterDefinition } from '@app/types/filter-definition'; -import { Filter, FilterInput, FilterType, FilterValueOptions } from '@app/types/filters'; +import { Filter, FilterInput, FilterType, FilterValueOptions, FiltersEnums, FiltersOptionsEnums } from '@app/types/filters'; +import { DataFilterService, FilterField } from '@app/types/services/data-filter.service'; import { AutoCompleteComponent } from '@components/auto-complete.component'; import { FiltersService } from '@services/filters.service'; import { FiltersDialogInputComponent } from './filters-dialog-input.component'; @@ -30,12 +29,10 @@ span { `], standalone: true, imports: [ - KeyValuePipe, MatFormFieldModule, MatSelectModule, FiltersDialogInputComponent, MatInputModule, - AsyncPipe, FormsModule, ReactiveFormsModule, AutoCompleteComponent, @@ -44,9 +41,9 @@ span { FiltersService, ], }) -export class FiltersDialogFilterFieldComponent<T extends number, U extends number | null = null> { +export class FiltersDialogFilterFieldComponent<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> { @Input({ required: true }) first: boolean; - @Input({ required: true }) set filter(entry: Filter<T, U>) { + @Input({ required: true }) set filter(entry: Filter<F, O>) { this._filter = entry; this.setStatuses(); this.setType(); @@ -60,7 +57,6 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe this.addCustomsToProperties(); } - allProperties: FilterDefinition<T, U>[]; labelledProperties: string[]; allOperators: Record<number, string>; @@ -70,9 +66,9 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe labelledStatuses: string[]; readonly filtersService = inject(FiltersService); - readonly dataFiltersService = inject(DATA_FILTERS_SERVICE); + readonly dataFiltersService = inject(DataFilterService); - _filter: Filter<T, U>; + _filter: Filter<F, O>; _customColumns: CustomColumn[]; columnValue: string; operatorLabel: string; @@ -83,14 +79,10 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe return this._filter; } - get filtersDefinitions() { - return this.dataFiltersService.retrieveFiltersDefinitions<T, U>(); - } - private setColumnValue() { if (this._filter.field && this._filter.for) { if (this._filter.for !== 'custom') { - this.columnValue = this.dataFiltersService.retrieveLabel(this._filter.for, this._filter.field); + this.columnValue = this.dataFiltersService.retrieveLabel(this._filter.for, this._filter.field as FilterField); } else { this.columnValue = this._filter.field.toString(); } @@ -104,8 +96,7 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe } private setProperties() { - this.allProperties = this.dataFiltersService.retrieveFiltersDefinitions<T, U>(); - this.labelledProperties = this.allProperties.map(property => this.retrieveLabel(property)); + this.labelledProperties = this.dataFiltersService.filtersDefinitions.map(property => this.retrieveLabel(property)); this.addCustomsToProperties(); } @@ -115,8 +106,8 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe } } - private retrieveLabel(filterDefinition: FilterDefinition<T, U>) { - return this.dataFiltersService.retrieveLabel(filterDefinition.for, filterDefinition.field); + private retrieveLabel(filterDefinition: FilterDefinition<F, O>) { + return this.dataFiltersService.retrieveLabel(filterDefinition.for, filterDefinition.field as FilterField); } private setOperators() { @@ -134,7 +125,7 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe this.labelledStatuses = Object.values(this.allStatuses).map(status => status.value); } - retrieveStatusKey(status: string) { + private retrieveStatusKey(status: string) { const key = this.allStatuses.find(label => label.value.toLowerCase() === status.toLowerCase())?.key; if (key) { return key; @@ -143,7 +134,7 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe } onPropertyChange(value: string) { - const field = this.dataFiltersService.retrieveField(value); + const field = this.dataFiltersService.retrieveField(value) as { for: string; index: number }; if (field.index === -1) { const customField = this._customColumns?.find(col => col.toLowerCase() === `options.options.${value.toLowerCase()}`); if (customField) { @@ -152,10 +143,10 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe this.filter.operator = null; } } else { - const for_ = this.allProperties.find(value => value.for === field.for && value.field === field.index)?.for; + const for_ = this.dataFiltersService.filtersDefinitions.find(value => value.for === field.for && value.field === field.index)?.for; if (for_) { this._filter.for = for_; - this._filter.field = field.index as T | U; + this._filter.field = field.index as F | O; this.filter.operator = null; } } @@ -171,7 +162,7 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe this._filter.operator = key !== undefined ? Number(key) : null; } - retrieveOperatorKey(operator: string) { + private retrieveOperatorKey(operator: string) { const labelledOperators = Object.values(this.allOperators); const value = labelledOperators.find(label => label.toLowerCase() === operator.toLowerCase()); return Object.keys(this.allOperators).find(key => this.allOperators[Number(key)] === value); @@ -188,7 +179,7 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe } } - setInput() { + private setInput() { switch (this.type) { case 'string': { this.input = { @@ -244,7 +235,7 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe } } - findType(): FilterType { + private findType(): FilterType { if (this._filter.field && this._filter.for !== 'custom') { const field = this.findFilterMetadata(this._filter); if (field) { @@ -254,7 +245,7 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe return 'string'; } - findStatuses(): FilterValueOptions { + private findStatuses(): FilterValueOptions { if (this._filter.field) { const field = this.findFilterMetadata(this._filter); if (field?.type === 'status') { @@ -264,11 +255,11 @@ export class FiltersDialogFilterFieldComponent<T extends number, U extends numbe return []; } - findOperators(): Record<number, string> { + private findOperators(): Record<number, string> { return this.filtersService.findOperators(this.type); } - findFilterMetadata(filter: Filter<T, U>): FilterDefinition<T, U> | null { - return this.dataFiltersService.retrieveFiltersDefinitions<T, U>().find(f => f.for === filter.for && f.field === filter.field) ?? null; + private findFilterMetadata(filter: Filter<F, O>): FilterDefinition<F, O> | null { + return this.dataFiltersService.filtersDefinitions.find(f => f.for === filter.for && f.field === filter.field) ?? null; } } diff --git a/src/app/components/filters/filters-dialog-input.component.ts b/src/app/components/filters/filters-dialog-input.component.ts index 551bfa996..179ffe62d 100644 --- a/src/app/components/filters/filters-dialog-input.component.ts +++ b/src/app/components/filters/filters-dialog-input.component.ts @@ -1,4 +1,3 @@ -import { AsyncPipe } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -25,7 +24,6 @@ mat-form-field { NgxMatTimepickerModule, NgxMatDatetimePickerModule, NgxMatNativeDateModule, - AsyncPipe, MatAutocompleteModule, MatButtonModule, AutoCompleteComponent diff --git a/src/app/components/filters/filters-dialog-or.component.spec.ts b/src/app/components/filters/filters-dialog-or.component.spec.ts index 0609ce1af..908e1eab2 100644 --- a/src/app/components/filters/filters-dialog-or.component.spec.ts +++ b/src/app/components/filters/filters-dialog-or.component.spec.ts @@ -1,20 +1,19 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TaskOptionEnumField, TaskSummaryEnumField } from '@aneoconsultingfr/armonik.api.angular'; +import { TestBed } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { Filter } from '@app/types/filters'; import { IconsService } from '@services/icons.service'; import { FiltersDialogOrComponent } from './filters-dialog-or.component'; describe('FiltersDialogOrComponent', () => { - let component: FiltersDialogOrComponent<number, number>; - let fixture: ComponentFixture<FiltersDialogOrComponent<number, number>>; - const filter1: Filter<number, number> = { + let component: FiltersDialogOrComponent<TaskSummaryEnumField, TaskOptionEnumField>; + const filter1: Filter<TaskSummaryEnumField, TaskOptionEnumField> = { field: 1, for: 'root', operator: 1, value: 'someValue' }; - const filter2: Filter<number, number> = { + const filter2: Filter<TaskSummaryEnumField, TaskOptionEnumField> = { field: 1, for: 'root', operator: 2, @@ -22,26 +21,14 @@ describe('FiltersDialogOrComponent', () => { }; beforeEach(async () => { - await TestBed.configureTestingModule({ + component = TestBed.configureTestingModule({ imports: [BrowserAnimationsModule], providers: [ FiltersDialogOrComponent, IconsService, - { provide: DATA_FILTERS_SERVICE, useValue: { - retrieveFiltersDefinitions: jest.fn(() => { - return []; - }), - retrieveLabel: jest.fn() - } }, ] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(FiltersDialogOrComponent<number, number>); - component = fixture.componentInstance; + }).inject(FiltersDialogOrComponent<TaskSummaryEnumField, TaskOptionEnumField>); component.filtersOr = [filter1, filter2]; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/components/filters/filters-dialog-or.component.ts b/src/app/components/filters/filters-dialog-or.component.ts index 88c5797e5..a3ddac6b8 100644 --- a/src/app/components/filters/filters-dialog-or.component.ts +++ b/src/app/components/filters/filters-dialog-or.component.ts @@ -3,7 +3,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { CustomColumn } from '@app/types/data'; -import { Filter } from '@app/types/filters'; +import { Filter, FiltersEnums, FiltersOptionsEnums } from '@app/types/filters'; import { IconsService } from '@services/icons.service'; import { FiltersDialogAndComponent } from './filters-dialog-and.component'; @@ -47,12 +47,12 @@ span { IconsService ], }) -export class FiltersDialogOrComponent<T extends number, U extends number | null = null> { +export class FiltersDialogOrComponent<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> { @Input({ required: true }) first: boolean; - @Input({ required: true }) filtersOr: Filter<T, U>[]; + @Input({ required: true }) filtersOr: Filter<F, O>[]; @Input() customColumns: CustomColumn[]; - @Output() removeChange: EventEmitter<Filter<T, U>[]> = new EventEmitter<Filter<T, U>[]>(); + @Output() removeChange: EventEmitter<Filter<F, O>[]> = new EventEmitter<Filter<F, O>[]>(); private readonly iconsService = inject(IconsService); @@ -69,7 +69,7 @@ export class FiltersDialogOrComponent<T extends number, U extends number | null }); } - onRemoveAnd(filter: Filter<T, U>) { + onRemoveAnd(filter: Filter<F, O>) { const index = this.filtersOr.indexOf(filter); if (index > -1) { this.filtersOr.splice(index, 1); diff --git a/src/app/components/filters/filters-dialog.component.spec.ts b/src/app/components/filters/filters-dialog.component.spec.ts index c636b564d..1df211dca 100644 --- a/src/app/components/filters/filters-dialog.component.spec.ts +++ b/src/app/components/filters/filters-dialog.component.spec.ts @@ -1,14 +1,12 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { FiltersAnd, FiltersOr } from '@app/types/filters'; import { IconsService } from '@services/icons.service'; import { FiltersDialogComponent } from './filters-dialog.component'; describe('FiltersDialogComponent', () => { let component: FiltersDialogComponent<number, number>; - let fixture: ComponentFixture<FiltersDialogComponent<number, number>>; const filterAnd1: FiltersAnd<number, number> = [{ field: 1, @@ -34,28 +32,17 @@ describe('FiltersDialogComponent', () => { }; beforeEach(async () => { - await TestBed.configureTestingModule({ + component = TestBed.configureTestingModule({ imports: [BrowserAnimationsModule], providers: [ FiltersDialogComponent, IconsService, { provide: MatDialogRef, useValue: mockDialogRef }, { provide: MAT_DIALOG_DATA, useValue: mockMatDialogData }, - { provide: DATA_FILTERS_SERVICE, useValue: { - retrieveFiltersDefinitions: jest.fn(() => { - return []; - }), - retrieveLabel: jest.fn() - } }, ] - }).compileComponents(); - }); - - beforeEach(() => { + }).inject(FiltersDialogComponent); mockMatDialogData.filtersOr = filterOr; - fixture = TestBed.createComponent(FiltersDialogComponent<number, number>); - component = fixture.componentInstance; - fixture.detectChanges(); + component.ngOnInit(); }); it('should init', () => { diff --git a/src/app/components/filters/filters-dialog.component.ts b/src/app/components/filters/filters-dialog.component.ts index 60c0feddd..d4fd00c2e 100644 --- a/src/app/components/filters/filters-dialog.component.ts +++ b/src/app/components/filters/filters-dialog.component.ts @@ -1,4 +1,3 @@ -import { KeyValuePipe } from '@angular/common'; import { Component, Inject, OnInit, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; @@ -10,10 +9,9 @@ import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; import { CustomColumn } from '@app/types/data'; import { FiltersDialogData } from '@app/types/dialog'; -import { Filter, FiltersOr } from '@app/types/filters'; +import { Filter, FiltersEnums, FiltersOptionsEnums, FiltersOr } from '@app/types/filters'; import { FiltersService } from '@services/filters.service'; import { IconsService } from '@services/icons.service'; -import { FiltersDialogInputComponent } from './filters-dialog-input.component'; import { FiltersDialogOrComponent } from './filters-dialog-or.component'; @Component({ @@ -28,9 +26,7 @@ import { FiltersDialogOrComponent } from './filters-dialog-or.component'; `], standalone: true, imports: [ - KeyValuePipe, FiltersDialogOrComponent, - FiltersDialogInputComponent, MatDialogModule, MatButtonModule, MatFormFieldModule, @@ -44,14 +40,14 @@ import { FiltersDialogOrComponent } from './filters-dialog-or.component'; FiltersService, ], }) -export class FiltersDialogComponent<T extends number, U extends number | null = null> implements OnInit { +export class FiltersDialogComponent<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> implements OnInit { private readonly iconsService = inject(IconsService); - private readonly dialogRef = inject(MatDialogRef<FiltersDialogComponent<T, U>>); + private readonly dialogRef = inject(MatDialogRef<FiltersDialogComponent<F, O>>); - filtersOr: FiltersOr<T, U> = []; + filtersOr: FiltersOr<F, O> = []; customColumns: CustomColumn[]; - constructor(@Inject(MAT_DIALOG_DATA) public data: FiltersDialogData<T, U>){} + constructor(@Inject(MAT_DIALOG_DATA) public data: FiltersDialogData<F, O>){} ngOnInit(): void { if (!this.data.filtersOr.length) { @@ -73,7 +69,7 @@ export class FiltersDialogComponent<T extends number, U extends number | null = ]); } - onRemoveOr(filters: Filter<T, U>[]) { + onRemoveOr(filters: Filter<F, O>[]) { const index = this.filtersOr.indexOf(filters); if (index > -1) { this.filtersOr.splice(index, 1); diff --git a/src/app/components/filters/filters-toolbar.component.ts b/src/app/components/filters/filters-toolbar.component.ts index d0847f7d4..d8a12520c 100644 --- a/src/app/components/filters/filters-toolbar.component.ts +++ b/src/app/components/filters/filters-toolbar.component.ts @@ -5,7 +5,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { CustomColumn } from '@app/types/data'; import { FiltersDialogData, FiltersDialogResult } from '@app/types/dialog'; -import { FiltersOr } from '@app/types/filters'; +import { FiltersEnums, FiltersOptionsEnums, FiltersOr } from '@app/types/filters'; import { FiltersChipsComponent } from '@components/filters/filters-chips.component'; import { IconsService } from '@services/icons.service'; import { FiltersDialogComponent } from './filters-dialog.component'; @@ -59,14 +59,13 @@ button { standalone: true, imports: [ FiltersChipsComponent, - FiltersDialogComponent, MatButtonModule, MatIconModule, MatDialogModule, MatTooltipModule, ], }) -export class FiltersToolbarComponent<T extends number, U extends number | null = null> { +export class FiltersToolbarComponent<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> { private readonly iconsService = inject(IconsService); private readonly dialog = inject(MatDialog); private readonly viewContainerRef = inject(ViewContainerRef); @@ -74,22 +73,22 @@ export class FiltersToolbarComponent<T extends number, U extends number | null = hasFilters = false; hasOneOrFilter = false; - private _filters: FiltersOr<T, U> = []; + private _filters: FiltersOr<F, O> = []; - @Input({ required: true }) set filters(entry: FiltersOr<T, U>) { + @Input({ required: true }) set filters(entry: FiltersOr<F, O>) { this._filters = entry; this.setHasFilters(); this.setHasOneOrFilter(); } - get filters(): FiltersOr<T, U> { + get filters(): FiltersOr<F, O> { return this._filters; } @Input() customColumns: CustomColumn[]; @Input() showFilters = true; - @Output() filtersChange: EventEmitter<FiltersOr<T, U>> = new EventEmitter<FiltersOr<T, U>>(); + @Output() filtersChange: EventEmitter<FiltersOr<F, O>> = new EventEmitter<FiltersOr<F, O>>(); @Output() showFiltersChange: EventEmitter<boolean> = new EventEmitter<boolean>(); getIcon(name: string): string { @@ -117,7 +116,7 @@ export class FiltersToolbarComponent<T extends number, U extends number | null = } openFiltersDialog(): void { - const dialogRef = this.dialog.open<FiltersDialogComponent<T, U>, FiltersDialogData<T, U>, FiltersDialogResult<T, U>>(FiltersDialogComponent, { + const dialogRef = this.dialog.open<FiltersDialogComponent<F, O>, FiltersDialogData<F, O>, FiltersDialogResult<F, O>>(FiltersDialogComponent, { data: { filtersOr: Array.from(this.filters), customColumns: this.customColumns @@ -140,7 +139,7 @@ export class FiltersToolbarComponent<T extends number, U extends number | null = }); } - isFilterNull(result: FiltersDialogResult<T, U>): boolean { + isFilterNull(result: FiltersDialogResult<F, O>): boolean { return result[0][0].field === null && result[0][0].for === null && result[0][0].operator === null && result[0][0].value === null; } } diff --git a/src/app/dashboard/components/lines/applications-line.component.ts b/src/app/dashboard/components/lines/applications-line.component.ts index 5bc40c44c..e4de80d3a 100644 --- a/src/app/dashboard/components/lines/applications-line.component.ts +++ b/src/app/dashboard/components/lines/applications-line.component.ts @@ -10,8 +10,8 @@ import { ApplicationsFiltersService } from '@app/applications/services/applicati import { ApplicationsGrpcService } from '@app/applications/services/applications-grpc.service'; import { ApplicationsIndexService } from '@app/applications/services/applications-index.service'; import { ApplicationRaw } from '@app/applications/types'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { DashboardLineTableComponent } from '@app/types/components/dashboard-line-table'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { TableDashboardActionsToolbarComponent } from '@components/table-dashboard-actions-toolbar.component'; import { AutoRefreshService } from '@services/auto-refresh.service'; @@ -34,7 +34,7 @@ import { ShareUrlService } from '@services/share-url.service'; DefaultConfigService, MatSnackBar, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useClass: ApplicationsFiltersService }, ApplicationsFiltersService, diff --git a/src/app/dashboard/components/lines/partitions-line.component.ts b/src/app/dashboard/components/lines/partitions-line.component.ts index 22bfafa0a..8d4f8cc35 100644 --- a/src/app/dashboard/components/lines/partitions-line.component.ts +++ b/src/app/dashboard/components/lines/partitions-line.component.ts @@ -10,8 +10,8 @@ import { PartitionsFiltersService } from '@app/partitions/services/partitions-fi import { PartitionsGrpcService } from '@app/partitions/services/partitions-grpc.service'; import { PartitionsIndexService } from '@app/partitions/services/partitions-index.service'; import { PartitionRaw } from '@app/partitions/types'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { DashboardLineTableComponent } from '@app/types/components/dashboard-line-table'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { TableDashboardActionsToolbarComponent } from '@components/table-dashboard-actions-toolbar.component'; import { GrpcSortFieldService } from '@services/grpc-sort-field.service'; @@ -27,7 +27,7 @@ import { NotificationService } from '@services/notification.service'; MatSnackBar, PartitionsFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: PartitionsFiltersService }, PartitionsGrpcService, diff --git a/src/app/dashboard/components/lines/results-line.component.ts b/src/app/dashboard/components/lines/results-line.component.ts index 126c9e1aa..e0d793d28 100644 --- a/src/app/dashboard/components/lines/results-line.component.ts +++ b/src/app/dashboard/components/lines/results-line.component.ts @@ -11,8 +11,8 @@ import { ResultsGrpcService } from '@app/results/services/results-grpc.service'; import { ResultsIndexService } from '@app/results/services/results-index.service'; import { ResultsStatusesService } from '@app/results/services/results-statuses.service'; import { ResultRaw } from '@app/results/types'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { DashboardLineTableComponent } from '@app/types/components/dashboard-line-table'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { TableDashboardActionsToolbarComponent } from '@components/table-dashboard-actions-toolbar.component'; import { FiltersService } from '@services/filters.service'; @@ -28,7 +28,7 @@ import { NotificationService } from '@services/notification.service'; ResultsIndexService, ResultsFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: ResultsFiltersService }, ResultsDataService, diff --git a/src/app/dashboard/components/lines/sessions-line.component.ts b/src/app/dashboard/components/lines/sessions-line.component.ts index 62d9f0a18..055436ae0 100644 --- a/src/app/dashboard/components/lines/sessions-line.component.ts +++ b/src/app/dashboard/components/lines/sessions-line.component.ts @@ -15,8 +15,8 @@ import { TasksFiltersService } from '@app/tasks/services/tasks-filters.service'; import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service'; import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service'; import { TaskOptions } from '@app/tasks/types'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { DashboardLineCustomColumnsComponent } from '@app/types/components/dashboard-line-table'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { TableDashboardActionsToolbarComponent } from '@components/table-dashboard-actions-toolbar.component'; import { FiltersService } from '@services/filters.service'; @@ -32,7 +32,7 @@ import { NotificationService } from '@services/notification.service'; NotificationService, SessionsFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: SessionsFiltersService }, SessionsIndexService, diff --git a/src/app/dashboard/components/lines/task-by-status-line.component.ts b/src/app/dashboard/components/lines/task-by-status-line.component.ts index ad5abff2b..9f2b155b8 100644 --- a/src/app/dashboard/components/lines/task-by-status-line.component.ts +++ b/src/app/dashboard/components/lines/task-by-status-line.component.ts @@ -9,8 +9,8 @@ import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service'; import { TasksIndexService } from '@app/tasks/services/tasks-index.service'; import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service'; import { StatusCount, TaskSummaryFilters } from '@app/tasks/types'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { EditNameLineData } from '@app/types/dialog'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { ActionsToolbarGroupComponent } from '@components/actions-toolbar-group.component'; import { ActionsToolbarComponent } from '@components/actions-toolbar.component'; import { AutoRefreshButtonComponent } from '@components/auto-refresh-button.component'; @@ -61,7 +61,7 @@ app-actions-toolbar { TasksGrpcService, TasksFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useClass: TasksFiltersService }, GrpcSortFieldService, diff --git a/src/app/dashboard/components/lines/tasks-line.component.ts b/src/app/dashboard/components/lines/tasks-line.component.ts index d02cfc60c..5435eb0af 100644 --- a/src/app/dashboard/components/lines/tasks-line.component.ts +++ b/src/app/dashboard/components/lines/tasks-line.component.ts @@ -12,9 +12,9 @@ import { TasksFiltersService } from '@app/tasks/services/tasks-filters.service'; import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service'; import { TasksIndexService } from '@app/tasks/services/tasks-index.service'; import { TaskOptions, TaskSummary } from '@app/tasks/types'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { DashboardLineCustomColumnsComponent } from '@app/types/components/dashboard-line-table'; import { ManageViewInLogsDialogData, ManageViewInLogsDialogResult } from '@app/types/dialog'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { TableDashboardActionsToolbarComponent } from '@components/table-dashboard-actions-toolbar.component'; import { FiltersService } from '@services/filters.service'; @@ -31,7 +31,7 @@ import { NotificationService } from '@services/notification.service'; NotificationService, TasksFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: TasksFiltersService }, TasksGrpcService, diff --git a/src/app/partitions/index.component.ts b/src/app/partitions/index.component.ts index 435527c54..6b617c490 100644 --- a/src/app/partitions/index.component.ts +++ b/src/app/partitions/index.component.ts @@ -9,8 +9,8 @@ import { DashboardIndexService } from '@app/dashboard/services/dashboard-index.s import { DashboardStorageService } from '@app/dashboard/services/dashboard-storage.service'; import { TasksIndexService } from '@app/tasks/services/tasks-index.service'; import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { TableHandler } from '@app/types/components'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { TableType } from '@app/types/table'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { PageHeaderComponent } from '@components/page-header.component'; @@ -54,7 +54,7 @@ import { PartitionRaw } from './types'; FiltersService, PartitionsFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: PartitionsFiltersService }, DashboardIndexService, diff --git a/src/app/partitions/services/partitions-filters.service.ts b/src/app/partitions/services/partitions-filters.service.ts index 87174a451..5b71520f1 100644 --- a/src/app/partitions/services/partitions-filters.service.ts +++ b/src/app/partitions/services/partitions-filters.service.ts @@ -2,13 +2,13 @@ import { PartitionRawEnumField } from '@aneoconsultingfr/armonik.api.angular'; import { Injectable } from '@angular/core'; import { Scope } from '@app/types/config'; import { FilterFor } from '@app/types/filter-definition'; -import { AbstractFilterService } from '@app/types/services/filtersService'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { PartitionFilterField, PartitionRawFilters, PartitionsFiltersDefinition } from '../types'; @Injectable({ providedIn: 'root' }) -export class PartitionsFiltersService extends AbstractFilterService<PartitionRawEnumField> { +export class PartitionsFiltersService extends DataFilterService<PartitionRawEnumField> { protected readonly scope: Scope = 'partitions'; readonly rootField: Record<PartitionRawEnumField, string> = { [PartitionRawEnumField.PARTITION_RAW_ENUM_FIELD_ID]: $localize`ID`, diff --git a/src/app/results/index.component.ts b/src/app/results/index.component.ts index 44bf383de..ebb4b5368 100644 --- a/src/app/results/index.component.ts +++ b/src/app/results/index.component.ts @@ -8,8 +8,8 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { DashboardIndexService } from '@app/dashboard/services/dashboard-index.service'; import { DashboardStorageService } from '@app/dashboard/services/dashboard-storage.service'; import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { TableHandler } from '@app/types/components'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { TableType } from '@app/types/table'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { PageHeaderComponent } from '@components/page-header.component'; @@ -48,7 +48,7 @@ import { ResultRaw } from './types'; AutoRefreshService, ResultsFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: ResultsFiltersService, }, ResultsStatusesService, diff --git a/src/app/results/services/results-filters.service.ts b/src/app/results/services/results-filters.service.ts index 5872765f9..4251c2879 100644 --- a/src/app/results/services/results-filters.service.ts +++ b/src/app/results/services/results-filters.service.ts @@ -2,14 +2,14 @@ import { ResultRawEnumField, ResultStatus } from '@aneoconsultingfr/armonik.api. import { Injectable, inject } from '@angular/core'; import { Scope } from '@app/types/config'; import { FilterFor } from '@app/types/filter-definition'; -import { AbstractFilterService, FiltersServiceStatusesInterface } from '@app/types/services/filtersService'; +import { DataFilterService, FiltersServiceStatusesInterface } from '@app/types/services/data-filter.service'; import { ResultsStatusesService } from './results-statuses.service'; import { ResultFilterField, ResultRawFilters, ResultsFiltersDefinition } from '../types'; @Injectable({ providedIn: 'root' }) -export class ResultsFiltersService extends AbstractFilterService<ResultRawEnumField> implements FiltersServiceStatusesInterface { +export class ResultsFiltersService extends DataFilterService<ResultRawEnumField> implements FiltersServiceStatusesInterface { protected readonly scope: Scope = 'results'; readonly statusService = inject(ResultsStatusesService); diff --git a/src/app/sessions/index.component.ts b/src/app/sessions/index.component.ts index 0b8a0136b..a31a11cb7 100644 --- a/src/app/sessions/index.component.ts +++ b/src/app/sessions/index.component.ts @@ -13,9 +13,9 @@ import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service'; import { TasksIndexService } from '@app/tasks/services/tasks-index.service'; import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service'; import { TaskOptions } from '@app/tasks/types'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { TableHandlerCustomValues } from '@app/types/components'; import { ColumnKey } from '@app/types/data'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { TableType } from '@app/types/table'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { PageHeaderComponent } from '@components/page-header.component'; @@ -59,7 +59,7 @@ import { SessionRaw } from './types'; TasksFiltersService, SessionsFiltersService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: SessionsFiltersService }, SessionsStatusesService, diff --git a/src/app/sessions/services/sessions-filters.service.ts b/src/app/sessions/services/sessions-filters.service.ts index dc2d6b41f..423e300a8 100644 --- a/src/app/sessions/services/sessions-filters.service.ts +++ b/src/app/sessions/services/sessions-filters.service.ts @@ -1,14 +1,14 @@ import { SessionRawEnumField, SessionStatus, SessionTaskOptionEnumField, TaskOptionEnumField } from '@aneoconsultingfr/armonik.api.angular'; import { Injectable, inject } from '@angular/core'; import { Scope } from '@app/types/config'; -import { AbstractFilterService, FiltersServiceOptionsInterface, FiltersServiceStatusesInterface } from '@app/types/services/filtersService'; +import { DataFilterService, FiltersServiceOptionsInterface, FiltersServiceStatusesInterface } from '@app/types/services/data-filter.service'; import { SessionsStatusesService } from './sessions-statuses.service'; import { SessionFilterDefinition, SessionFilterField, SessionFilterFor, SessionRawFilters } from '../types'; @Injectable({ providedIn: 'root', }) -export class SessionsFiltersService extends AbstractFilterService<SessionRawEnumField, TaskOptionEnumField> +export class SessionsFiltersService extends DataFilterService<SessionRawEnumField, TaskOptionEnumField> implements FiltersServiceOptionsInterface<TaskOptionEnumField>, FiltersServiceStatusesInterface { protected readonly scope: Scope = 'sessions'; readonly statusService = inject(SessionsStatusesService); diff --git a/src/app/tasks/index.component.ts b/src/app/tasks/index.component.ts index 8bb03cd61..b69d76253 100644 --- a/src/app/tasks/index.component.ts +++ b/src/app/tasks/index.component.ts @@ -7,9 +7,9 @@ import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatToolbarModule } from '@angular/material/toolbar'; import { DashboardIndexService } from '@app/dashboard/services/dashboard-index.service'; import { DashboardStorageService } from '@app/dashboard/services/dashboard-storage.service'; -import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; import { TableHandlerCustomValues } from '@app/types/components'; import { ManageViewInLogsDialogData, ManageViewInLogsDialogResult } from '@app/types/dialog'; +import { DataFilterService } from '@app/types/services/data-filter.service'; import { TableType } from '@app/types/table'; import { FiltersToolbarComponent } from '@components/filters/filters-toolbar.component'; import { PageHeaderComponent } from '@components/page-header.component'; @@ -55,7 +55,7 @@ import { TaskOptions, TaskSummary, TaskSummaryFilter } from './types'; TasksStatusesService, TasksIndexService, { - provide: DATA_FILTERS_SERVICE, + provide: DataFilterService, useExisting: TasksFiltersService }, TableService, diff --git a/src/app/tasks/services/tasks-filters.service.ts b/src/app/tasks/services/tasks-filters.service.ts index 07e5b4dba..2205a081f 100644 --- a/src/app/tasks/services/tasks-filters.service.ts +++ b/src/app/tasks/services/tasks-filters.service.ts @@ -1,14 +1,14 @@ import { TaskOptionEnumField, TaskStatus, TaskSummaryEnumField } from '@aneoconsultingfr/armonik.api.angular'; import { Injectable, inject } from '@angular/core'; import { Scope } from '@app/types/config'; -import { AbstractFilterService, FiltersServiceOptionsInterface, FiltersServiceStatusesInterface } from '@app/types/services/filtersService'; +import { DataFilterService, FiltersServiceOptionsInterface, FiltersServiceStatusesInterface } from '@app/types/services/data-filter.service'; import { TasksStatusesService } from './tasks-statuses.service'; import { TaskFilterDefinition, TaskFilterField, TaskFilterFor, TaskSummaryFilters } from '../types'; @Injectable({ providedIn: 'root' }) -export class TasksFiltersService extends AbstractFilterService<TaskSummaryEnumField, TaskOptionEnumField> +export class TasksFiltersService extends DataFilterService<TaskSummaryEnumField, TaskOptionEnumField> implements FiltersServiceOptionsInterface<TaskOptionEnumField>, FiltersServiceStatusesInterface { protected readonly scope: Scope = 'tasks'; readonly statusService = inject(TasksStatusesService); diff --git a/src/app/tokens/filters.token.ts b/src/app/tokens/filters.token.ts deleted file mode 100644 index aeb34a2c6..000000000 --- a/src/app/tokens/filters.token.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { InjectionToken } from '@angular/core'; -import { DataFilterService } from '@app/types/filter-definition'; - -export const DATA_FILTERS_SERVICE = new InjectionToken<DataFilterService>('DATA_FILTERS_SERVICE'); diff --git a/src/app/types/components/index.ts b/src/app/types/components/index.ts index 0668785f3..4a24df0a4 100644 --- a/src/app/types/components/index.ts +++ b/src/app/types/components/index.ts @@ -12,7 +12,7 @@ import { Observable, Subject, Subscription } from 'rxjs'; import { TableColumn } from '../column.type'; import { ColumnKey, CustomColumn, DataRaw } from '../data'; import { FiltersEnums, FiltersOptionsEnums, FiltersOr } from '../filters'; -import { AbstractFilterService } from '../services/filtersService'; +import { DataFilterService } from '../services/data-filter.service'; import { IndexServiceCustomInterface, IndexServiceInterface } from '../services/indexService'; import { AbstractTableDataService } from '../services/table-data.service'; import { TableType } from '../table'; @@ -26,7 +26,7 @@ export abstract class TableHandler<T extends DataRaw, F extends FiltersEnums, O readonly router = inject(Router); abstract readonly indexService: IndexServiceInterface<T, O>; - abstract readonly filtersService: AbstractFilterService<F, FO>; + abstract readonly filtersService: DataFilterService<F, FO>; abstract readonly tableDataService: AbstractTableDataService<T, F, O, FO>; abstract tableType: TableType; diff --git a/src/app/types/filter-definition.ts b/src/app/types/filter-definition.ts index dd908b6ab..e831c4160 100644 --- a/src/app/types/filter-definition.ts +++ b/src/app/types/filter-definition.ts @@ -1,4 +1,4 @@ -import { FilterValueOptions, FiltersAnd } from '@app/types/filters'; +import { FilterValueOptions } from '@app/types/filters'; /** * * `for` and `field` are used to identify the filter. @@ -98,15 +98,4 @@ export type FilterDefinitionTaskOption<T extends number | null> = FilterDefiniti export type FilterDefinition<T extends number, U extends number | null = null> = FilterDefinitionRoot<T> | FilterDefinitionTaskOption<U> | FilterDefinitionCustom; -export type FilterFor<T extends number, U extends number | null = null> = FilterDefinition<T, U>['for']; - - - -export abstract class DataFilterService { - abstract retrieveFiltersDefinitions<T extends number, U extends number | null = null>(): FilterDefinition<T, U>[]; - abstract retrieveLabel<T extends number, U extends number | null = null>(filterFor: FilterFor<T, U>, filterField: T | U | string): string; - abstract saveFilters<T extends number, U extends number | null = null>(filters: FiltersAnd<T, U>[]): void; - abstract restoreFilters<T extends number, U extends number | null = null>(): FiltersAnd<T, U>[]; - abstract resetFilters<T extends number, U extends number | null = null>(): FiltersAnd<T, U>[]; - abstract retrieveField<U extends { for: string, index: number }>(filterField: string): U; -} \ No newline at end of file +export type FilterFor<T extends number, U extends number | null = null> = FilterDefinition<T, U>['for']; \ No newline at end of file diff --git a/src/app/types/services/filtersService.ts b/src/app/types/services/data-filter.service.ts similarity index 94% rename from src/app/types/services/filtersService.ts rename to src/app/types/services/data-filter.service.ts index d337f76bc..5c6905e62 100644 --- a/src/app/types/services/filtersService.ts +++ b/src/app/types/services/data-filter.service.ts @@ -18,7 +18,7 @@ type StatusesService = TasksStatusesService | ResultsStatusesService | SessionsS export type FilterFor = TaskFilterFor | ResultFilterFor | SessionFilterFor | PartitionFilterFor | ApplicationFilterFor; export type FilterField = TaskFilterField | ResultFilterField | SessionFilterField | PartitionFilterField | ApplicationFilterField; -export abstract class AbstractFilterService<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> { +export abstract class DataFilterService<F extends FiltersEnums, O extends FiltersOptionsEnums | null = null> { protected abstract readonly scope: Scope; protected readonly defaultConfigService = inject(DefaultConfigService); private readonly tableService = inject(TableService); @@ -59,6 +59,7 @@ export abstract class AbstractFilterService<F extends FiltersEnums, O extends Fi } abstract retrieveLabel(filterFor: FilterFor, filterField: FilterField): string; + abstract retrieveField(filterField: string): FilterField; } export interface FiltersServiceOptionsInterface<O extends NonNullable<FiltersOptionsEnums>> { diff --git a/src/app/types/services/grpcService.ts b/src/app/types/services/grpcService.ts index 7e7eed10f..61246e397 100644 --- a/src/app/types/services/grpcService.ts +++ b/src/app/types/services/grpcService.ts @@ -8,7 +8,7 @@ import { DataRaw, FieldKey, GrpcResponse } from '../data'; import { FilterDefinition } from '../filter-definition'; import { Filter, FilterType, FiltersAnd, FiltersEnums, FiltersOptionsEnums, FiltersOr } from '../filters'; import { ListOptions } from '../options'; -import { AbstractFilterService } from './filtersService'; +import { DataFilterService } from './data-filter.service'; export type GrpcClient = TasksClient | ApplicationsClient | ResultsClient | SessionsClient | PartitionsClient; export type GetResponse = GetTaskResponse | GetPartitionResponse | GetResultResponse | GetSessionResponse; @@ -50,7 +50,7 @@ export abstract class GrpcTableService<T extends DataRaw, F extends FiltersEnums abstract readonly sortFields: Record<FieldKey<T>, F>; abstract readonly grpcClient: GrpcClient; - abstract readonly filterService: AbstractFilterService<F, FO>; + abstract readonly filterService: DataFilterService<F, FO>; readonly utilsService = inject(UtilsService<F, FO>);