diff --git a/libs/angular-accelerator/src/index.ts b/libs/angular-accelerator/src/index.ts index 2adb5339..37f5da0c 100644 --- a/libs/angular-accelerator/src/index.ts +++ b/libs/angular-accelerator/src/index.ts @@ -55,6 +55,7 @@ export * from './lib/functions/at-least-one-field-filled-validator' export * from './lib/utils/async-translate-loader.utils' export * from './lib/utils/caching-translate-loader.utils' export * from './lib/utils/colorutils' +export * from './lib/utils/data-operation-strategy' export * from './lib/utils/create-translate-loader.utils' export * from './lib/utils/dateutils' export * from './lib/utils/objectutils' diff --git a/libs/angular-accelerator/src/lib/components/data-sort-base/data-sort-base.ts b/libs/angular-accelerator/src/lib/components/data-sort-base/data-sort-base.ts index 42856df1..07ef787b 100644 --- a/libs/angular-accelerator/src/lib/components/data-sort-base/data-sort-base.ts +++ b/libs/angular-accelerator/src/lib/components/data-sort-base/data-sort-base.ts @@ -80,11 +80,13 @@ export class DataSortBase { )?.toString() switch (filter.filterType) { case undefined: - case FilterType.EQUAL: + case FilterType.EQUALS: return value === String(filter.value) - case FilterType.TRUTHY: { + case FilterType.IS_NOT_EMPTY: { return filter.value ? !!value : !value } + default: + return true } }) ) diff --git a/libs/angular-accelerator/src/lib/components/data-table/data-table.component.html b/libs/angular-accelerator/src/lib/components/data-table/data-table.component.html index e9fa0e7d..2ac56695 100644 --- a/libs/angular-accelerator/src/lib/components/data-table/data-table.component.html +++ b/libs/angular-accelerator/src/lib/components/data-table/data-table.component.html @@ -164,7 +164,7 @@ - filter.columnId === currentFilterColumn?.id && currentFilterColumn.filterType === FilterType.TRUTHY + filter.columnId === currentFilterColumn?.id && currentFilterColumn.filterType === FilterType.IS_NOT_EMPTY ) .map((filter) => filter.value) }) @@ -465,7 +465,7 @@ export class DataTableComponent extends DataSortBase implements OnInit, AfterCon .filter( (filter) => filter.columnId === currentFilterColumn?.id && - (!currentFilterColumn.filterType || currentFilterColumn.filterType === FilterType.EQUAL) + (!currentFilterColumn.filterType || currentFilterColumn.filterType === FilterType.EQUALS) ) .map((filter) => filter.value) }) @@ -473,7 +473,7 @@ export class DataTableComponent extends DataSortBase implements OnInit, AfterCon this.currentEqualFilterOptions$ = combineLatest([this._rows$, this.currentFilterColumn$, this._filters$]).pipe( filter( ([_, currentFilterColumn, __]) => - !currentFilterColumn?.filterType || currentFilterColumn.filterType === FilterType.EQUAL + !currentFilterColumn?.filterType || currentFilterColumn.filterType === FilterType.EQUALS ), mergeMap(([rows, currentFilterColumn, filters]) => { if (!currentFilterColumn?.id) { @@ -484,7 +484,7 @@ export class DataTableComponent extends DataSortBase implements OnInit, AfterCon .filter( (filter) => filter.columnId === currentFilterColumn?.id && - (!currentFilterColumn.filterType || currentFilterColumn.filterType === FilterType.EQUAL) + (!currentFilterColumn.filterType || currentFilterColumn.filterType === FilterType.EQUALS) ) .map((filter) => filter.value) diff --git a/libs/angular-accelerator/src/lib/components/filter-view/filter-view.component.html b/libs/angular-accelerator/src/lib/components/filter-view/filter-view.component.html index 17d80fad..e1e3df80 100644 --- a/libs/angular-accelerator/src/lib/components/filter-view/filter-view.component.html +++ b/libs/angular-accelerator/src/lib/components/filter-view/filter-view.component.html @@ -45,7 +45,7 @@ style="white-space: nowrap" class="p-chip-text flex flex-nowrap" >{{column?.nameKey ?? '' | translate }}: - + { diff --git a/libs/angular-accelerator/src/lib/components/interactive-data-view/storybook-config.ts b/libs/angular-accelerator/src/lib/components/interactive-data-view/storybook-config.ts index 47b33c6f..41d6284f 100644 --- a/libs/angular-accelerator/src/lib/components/interactive-data-view/storybook-config.ts +++ b/libs/angular-accelerator/src/lib/components/interactive-data-view/storybook-config.ts @@ -139,7 +139,7 @@ export const defaultInteractiveDataViewArgs = { nameKey: 'Available', sortable: false, filterable: true, - filterType: FilterType.TRUTHY, + filterType: FilterType.IS_NOT_EMPTY, predefinedGroupKeys: ['test2'], }, { diff --git a/libs/angular-accelerator/src/lib/model/filter.model.ts b/libs/angular-accelerator/src/lib/model/filter.model.ts index 8a6c34dd..79adb661 100644 --- a/libs/angular-accelerator/src/lib/model/filter.model.ts +++ b/libs/angular-accelerator/src/lib/model/filter.model.ts @@ -2,9 +2,21 @@ export interface ColumnFilterDataSelectOptions { reverse: boolean } -export type Filter = { columnId: string; value: unknown; filterType?: FilterType } +export type FilterObject = { columnId: string; filterType?: FilterType } + +export type Filter = FilterObject & { value: unknown } export enum FilterType { - EQUAL = 'EQUAL', - TRUTHY = 'TRUTHY', + ENDS_WITH = 'endsWith', + STARTS_WITH = 'startsWith', + CONTAINS = 'contains', + NOT_CONTAINS = 'notContains', + EQUALS = 'equals', + NOT_EQUALS = 'notEquals', + LESS_THAN = 'lessThan', + GREATER_THAN = 'greaterThan', + LESS_THAN_OR_EQUAL = 'lessThanOrEqual', + GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual', + IS_EMPTY = 'isEmpty', + IS_NOT_EMPTY = 'isNotEmpty', } diff --git a/libs/angular-accelerator/src/lib/utils/data-operation-strategy.spec.ts b/libs/angular-accelerator/src/lib/utils/data-operation-strategy.spec.ts new file mode 100644 index 00000000..f73c5571 --- /dev/null +++ b/libs/angular-accelerator/src/lib/utils/data-operation-strategy.spec.ts @@ -0,0 +1,334 @@ +import { TestBed } from '@angular/core/testing' +import { DataOperationStrategy } from './data-operation-strategy' +import { DataTableColumn } from '../model/data-table-column.model' +import { FilterObject, FilterType } from '../model/filter.model' +import { ColumnType } from '../model/column-type.model' + +/* eslint-disable @typescript-eslint/no-unused-vars */ +class NumberOperationStrategy extends DataOperationStrategy { + override equals(column: DataTableColumn, value: unknown, target: unknown): boolean { + return Number(value) === Number(target) + } + override lessThan(column: DataTableColumn, value: unknown, target: unknown): boolean { + return Number(value) < Number(target) + } + override compare(a: unknown, b: unknown, column: DataTableColumn): number { + return Number(a) - Number(b) + } +} + +class DateOperationStrategy extends DataOperationStrategy { + override equals(column: DataTableColumn, value: unknown, target: unknown): boolean { + if (!value || !(value instanceof Date) || !(target instanceof Date)) return false + // different implementation based on the column + let precision: 'day' | 'year' = 'year' + if (column.id === 'dayCol') precision = 'day' + + if (precision === 'day') { + return ( + value.getFullYear() === target.getFullYear() && + value.getMonth() === target.getMonth() && + value.getDate() === target.getDate() + ) + } + return value.getFullYear() === target.getFullYear() + } + + override isNotEmpty(column: DataTableColumn, value: unknown): boolean { + return !!value + } + + override compare(a: Date, b: Date, column: DataTableColumn): number { + let precision: 'day' | 'year' = 'year' + if (column.id === 'dayCol') precision = 'day' + + const aYear = a.getFullYear() + const aMonth = a.getMonth() + const aDay = a.getDate() + + const bYear = b.getFullYear() + const bMonth = b.getMonth() + const bDay = b.getDate() + + if (aYear !== bYear || precision === 'year') { + return aYear - bYear + } + if (aMonth !== bMonth) { + return aMonth - bMonth + } + return aDay - bDay + } + + override filterOptions(hayStack: unknown[], filterObject: FilterObject, columns: DataTableColumn[]) { + if (filterObject.filterType === FilterType.IS_NOT_EMPTY) { + return ['yes', 'no'] + } + + const hayStackValues = hayStack + .map((item) => this.mapHaystackItemToValue(item, filterObject)) + .filter((item) => !!item) + const column = columns.find((c) => c.id === filterObject.columnId) + if (!column) { + console.warn('Filter does not have a column id set. All items will be considered a valid option') + return hayStackValues + } + + let precision: 'day' | 'year' = 'year' + if (column.id === 'dayCol') precision = 'day' + + return hayStackValues.filter( + (item, index, self) => index === self.findIndex((t) => this.compare(t, item, column) === 0) + ) + } +} + +describe('DataOperationStrategy', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [], + }).compileComponents() + }) + + describe('NumberOperationStrategy', () => { + const items = [ + { + col: 1, + }, + { + col: 2, + }, + { + col: 3, + }, + { + col: 2, + }, + { + col: 4, + }, + ] + const strategy = new NumberOperationStrategy() + const columns: DataTableColumn[] = [ + { + id: 'col', + nameKey: '', + columnType: ColumnType.NUMBER, + }, + ] + + it('should result in equal numbers for filter', () => { + const result = strategy.filter( + items, + { + value: 2, + filterType: FilterType.EQUALS, + columnId: 'col', + }, + columns + ) + expect(result).toEqual([{ col: 2 }, { col: 2 }]) + }) + + it('should result in lower numbers for filter', () => { + const result = strategy.filter( + items, + { + value: 3, + filterType: FilterType.LESS_THAN, + columnId: 'col', + }, + columns + ) + expect(result).toEqual([{ col: 1 }, { col: 2 }, { col: 2 }]) + }) + + it('should result in unique numbers for filterOptions', () => { + const result = strategy.filterOptions( + items, + { + filterType: FilterType.LESS_THAN, + columnId: 'col', + }, + columns + ) + expect(result).toEqual([1, 2, 3, 4]) + }) + + it('should return all items for filter if filter type not set', () => { + const result = strategy.filter( + items, + { + value: 3, + columnId: 'col', + }, + columns + ) + expect(result).toEqual(items) + }) + it('should return all items for filter if column not found', () => { + const result = strategy.filter( + items, + { + value: 3, + filterType: FilterType.EQUALS, + columnId: 'col', + }, + [] + ) + expect(result).toEqual(items) + }) + it('should return all items for filterOptions if column not found', () => { + const result = strategy.filterOptions( + items, + { + filterType: FilterType.LESS_THAN, + columnId: 'col', + }, + [] + ) + expect(result).toEqual(items.map((i) => i.col)) + }) + }) + + describe('DateOperationStrategy', () => { + const items = [ + { + yearCol: new Date(2020, 1, 13), + dayCol: new Date(2020, 1, 13), + }, + { + yearCol: new Date(2020, 1, 13), + dayCol: new Date(2020, 1, 13), + }, + { + yearCol: new Date(2021, 1, 13), + dayCol: new Date(2021, 1, 13), + }, + { + yearCol: new Date(2022, 7, 20), + dayCol: new Date(2022, 7, 20), + }, + { + yearCol: new Date(2022, 1, 13), + dayCol: new Date(2022, 1, 13), + }, + { + yearCol: new Date(2024, 7, 20), + dayCol: new Date(2024, 7, 20), + }, + { + yearCol: undefined, + dayCol: undefined, + }, + ] + const strategy = new DateOperationStrategy() + const columns: DataTableColumn[] = [ + { + id: 'yearCol', + nameKey: '', + columnType: ColumnType.DATE, + }, + { + id: 'dayCol', + nameKey: '', + columnType: ColumnType.DATE, + }, + ] + const yearCol = 'yearCol' + const dayCol = 'dayCol' + + it('should result in equal dates with year precision', () => { + const result = strategy.filter( + items, + { + columnId: yearCol, + filterType: FilterType.EQUALS, + value: new Date(2022, 7, 20), + }, + columns + ) + + expect(result).toEqual([ + { yearCol: new Date(2022, 7, 20), dayCol: new Date(2022, 7, 20) }, + { yearCol: new Date(2022, 1, 13), dayCol: new Date(2022, 1, 13) }, + ]) + }) + + it('should result in equal dates with day precision', () => { + const result = strategy.filter( + items, + { + columnId: dayCol, + filterType: FilterType.EQUALS, + value: new Date(2020, 1, 13), + }, + columns + ) + + expect(result).toEqual([ + { yearCol: new Date(2020, 1, 13), dayCol: new Date(2020, 1, 13) }, + { yearCol: new Date(2020, 1, 13), dayCol: new Date(2020, 1, 13) }, + ]) + }) + it('should result in non empty dates', () => { + const result = strategy.filter( + items, + { + columnId: yearCol, + filterType: FilterType.IS_NOT_EMPTY, + value: 'yes', + }, + columns + ) + + expect(result.length).toEqual(items.length - 1) + }) + it('should result in unique dates with year precision', () => { + const result = strategy.filterOptions( + items, + { + columnId: yearCol, + filterType: FilterType.EQUALS, + }, + columns + ) + + expect(result).toEqual([ + new Date(2020, 1, 13), + new Date(2021, 1, 13), + new Date(2022, 7, 20), + new Date(2024, 7, 20), + ]) + }) + it('should result in unique dates with day precision', () => { + const result = strategy.filterOptions( + items, + { + columnId: dayCol, + filterType: FilterType.EQUALS, + }, + columns + ) + + expect(result).toEqual([ + new Date(2020, 1, 13), + new Date(2021, 1, 13), + new Date(2022, 7, 20), + new Date(2022, 1, 13), + new Date(2024, 7, 20), + ]) + }) + it('should result in yes and no options with not empty filter', () => { + const result = strategy.filterOptions( + items, + { + columnId: dayCol, + filterType: FilterType.IS_NOT_EMPTY, + }, + columns + ) + + expect(result).toEqual(['yes', 'no']) + }) + }) +}) diff --git a/libs/angular-accelerator/src/lib/utils/data-operation-strategy.ts b/libs/angular-accelerator/src/lib/utils/data-operation-strategy.ts new file mode 100644 index 00000000..9863f7d5 --- /dev/null +++ b/libs/angular-accelerator/src/lib/utils/data-operation-strategy.ts @@ -0,0 +1,101 @@ +import { DataTableColumn } from '../model/data-table-column.model' +import { Filter, FilterObject } from '../model/filter.model' +import { ObjectUtils } from './objectutils' + +/* eslint-disable @typescript-eslint/no-unused-vars */ +export abstract class DataOperationStrategy { + endsWith(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('endsWith method not implemented') + return true + } + + startsWith(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('startsWith method not implemented') + return true + } + + contains(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('contains method not implemented') + return true + } + + notContains(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('notContains method not implemented') + return true + } + + equals(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('equals method not implemented') + return true + } + + notEquals(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('notEquals method not implemented') + return true + } + + lessThan(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('lessThan method not implemented') + return true + } + + greaterThan(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('greaterThan method not implemented') + return true + } + + lessThanOrEqual(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('lessThanOrEqual method not implemented') + return true + } + + greaterThanOrEqual(column: DataTableColumn, value: unknown, target: unknown): boolean { + console.error('greaterThanOrEqual method not implemented') + return true + } + + isEmpty(column: DataTableColumn, value: unknown): boolean { + console.error('isEmpty method not implemented') + return true + } + + isNotEmpty(column: DataTableColumn, value: unknown): boolean { + console.error('isNotEmpty method not implemented') + return true + } + + compare(a: unknown, b: unknown, column: DataTableColumn): number { + console.error('compare method not implemented') + return 0 + } + + filterOptions(hayStack: unknown[], filterObject: FilterObject, columns: DataTableColumn[]): unknown[] { + const hayStackOptions = hayStack.map((item) => this.mapHaystackItemToValue(item, filterObject)) + const column = columns.find((c) => c.id === filterObject.columnId) + if (!column) { + console.warn('Filter does not have a column id set. All items will be considered a valid option') + return hayStackOptions + } + return hayStackOptions.filter( + (item, index, self) => index === self.findIndex((t) => this.compare(t, item, column) === 0) + ) + } + + filter(hayStack: unknown[], filter: Filter, columns: DataTableColumn[]): unknown[] { + const { filterType, value } = filter + if (!filterType) { + console.warn('Filter does not have a type set. All items will resolve as true') + return hayStack + } + const column = columns.find((c) => c.id === filter.columnId) + if (!column) { + console.warn('Filter does not have a column id set. All items will be considered a valid option') + return hayStack + } + return hayStack.filter((item) => this[filterType](column, this.mapHaystackItemToValue(item, filter), value)) + } + + mapHaystackItemToValue(item: unknown, filter: Filter | FilterObject) { + return ObjectUtils.resolveFieldData(item, filter.columnId) + } +}