@open-dcp/grid 0.0.0-stage → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.github/workflows/main.yml +42 -0
  2. package/CLA.md +22 -0
  3. package/LICENSE +190 -0
  4. package/NOTICE.md +31 -0
  5. package/README.md +39 -3
  6. package/SECURITY.md +27 -0
  7. package/dist/GridService.d.ts +83 -0
  8. package/dist/GridService.js +410 -0
  9. package/dist/__tests__/GridServices.test.d.ts +1 -0
  10. package/dist/__tests__/GridServices.test.js +288 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +1 -0
  13. package/dist/services/FilteringService.d.ts +34 -0
  14. package/dist/services/FilteringService.js +278 -0
  15. package/dist/services/FormattingService.d.ts +17 -0
  16. package/dist/services/FormattingService.js +179 -0
  17. package/dist/services/RowHeightService.d.ts +15 -0
  18. package/dist/services/RowHeightService.js +63 -0
  19. package/dist/services/SelectionService.d.ts +14 -0
  20. package/dist/services/SelectionService.js +55 -0
  21. package/dist/services/SortingService.d.ts +15 -0
  22. package/dist/services/SortingService.js +82 -0
  23. package/dist/services/__tests__/FilteringService.test.d.ts +1 -0
  24. package/dist/services/__tests__/FilteringService.test.js +140 -0
  25. package/dist/services/__tests__/FormattingService.test.d.ts +1 -0
  26. package/dist/services/__tests__/FormattingService.test.js +122 -0
  27. package/dist/types/grid.d.ts +71 -0
  28. package/dist/types/grid.js +1 -0
  29. package/dist/types/sorting.d.ts +8 -0
  30. package/dist/types/sorting.js +1 -0
  31. package/jest.config.js +16 -0
  32. package/package.json +43 -4
  33. package/reports/junit.xml +3 -0
  34. package/src/GridService.ts +561 -0
  35. package/src/__tests__/GridServices.test.ts +348 -0
  36. package/src/index.ts +17 -0
  37. package/src/services/FilteringService.ts +347 -0
  38. package/src/services/FormattingService.ts +205 -0
  39. package/src/services/RowHeightService.ts +81 -0
  40. package/src/services/SelectionService.ts +71 -0
  41. package/src/services/SortingService.ts +105 -0
  42. package/src/services/__tests__/FilteringService.test.ts +173 -0
  43. package/src/services/__tests__/FormattingService.test.ts +157 -0
  44. package/src/types/grid.ts +84 -0
  45. package/src/types/sorting.ts +9 -0
  46. package/tsconfig.json +19 -0
@@ -0,0 +1,179 @@
1
+ import { format } from 'date-fns';
2
+ import { toZonedTime } from 'date-fns-tz';
3
+ import { logger } from "@open-dcp/utilities";
4
+ function formatInTimeZone(timestamp, timeZone, formatString) {
5
+ const date = typeof timestamp === "number" ? new Date(timestamp) : timestamp;
6
+ const zonedDate = toZonedTime(date, timeZone);
7
+ return format(zonedDate, formatString);
8
+ }
9
+ export class FormattingService {
10
+ constructor(gridService) {
11
+ this.gridService = gridService;
12
+ }
13
+ applyFormattingToRow(row) {
14
+ const columns = this.gridService.getColumns();
15
+ const formattedRow = Object.assign({}, row); // Clone the row to keep it immutable
16
+ if (columns && columns.length > 0) {
17
+ // Columns are ready, proceed with formatting
18
+ columns.forEach((column) => {
19
+ var _a;
20
+ const cellData = (_a = row[column.key]) !== null && _a !== void 0 ? _a : null;
21
+ const rawValue = cellData && typeof cellData === 'object' && 'raw' in cellData
22
+ ? cellData.raw
23
+ : cellData;
24
+ const value = this.formatCellValue(row, column);
25
+ // Store raw and value
26
+ formattedRow[column.key] = {
27
+ raw: rawValue, // Ensure raw value stays the same
28
+ value: value, // Store transformed value for sorting/filtering
29
+ };
30
+ });
31
+ }
32
+ else {
33
+ Object.keys(row).forEach(key => {
34
+ formattedRow[key] = {
35
+ raw: row[key], // Use original value as raw
36
+ value: null, // No transformation yet
37
+ };
38
+ });
39
+ //logger.warn('Columns are not available yet. Row formatting is deferred.');
40
+ }
41
+ return formattedRow;
42
+ }
43
+ formatCustomCell(cellValue, row, column) {
44
+ let displayValue = undefined;
45
+ let renderedElement = undefined;
46
+ if (typeof column.renderCell === 'function') {
47
+ try {
48
+ const functionReturn = column.renderCell(cellValue, row);
49
+ if (functionReturn instanceof HTMLAnchorElement) {
50
+ renderedElement = functionReturn;
51
+ }
52
+ else if (typeof functionReturn == "string") {
53
+ displayValue = functionReturn;
54
+ }
55
+ }
56
+ catch (error) {
57
+ logger.error(`Error in custom renderCell function for column ${column.key}:`, error);
58
+ displayValue = 'Error';
59
+ }
60
+ }
61
+ else {
62
+ logger.warn(`Custom column type requires a renderCell function.`);
63
+ displayValue = 'N/A';
64
+ }
65
+ return { display: displayValue, renderedElement };
66
+ }
67
+ formatDateCell(cellValue, column) {
68
+ const date = new Date(cellValue);
69
+ let dateFormatString = this.gridService.displayFormatSubject.getValue().dateFormat;
70
+ if (column.type === 'datetime') {
71
+ dateFormatString += ` ${this.gridService.displayFormatSubject.getValue().timeFormat}`;
72
+ }
73
+ return formatInTimeZone(date, this.gridService.displayFormatSubject.getValue().timeZone, dateFormatString);
74
+ }
75
+ formatCellDisplay(row, column) {
76
+ var _a, _b;
77
+ const cellValue = (_b = (_a = row[column.key]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : null;
78
+ if (cellValue === null) {
79
+ return { display: null, renderedElement: undefined };
80
+ }
81
+ let displayValue = undefined;
82
+ let renderedElement = undefined;
83
+ if (typeof column.transformer === 'function') {
84
+ try {
85
+ const transformed = column.transformer(cellValue);
86
+ displayValue = transformed != null ? transformed.toString() : '';
87
+ return { display: displayValue, renderedElement };
88
+ }
89
+ catch (error) {
90
+ logger.error(`Error in transformer for column ${column.key}:`, error);
91
+ return { display: 'Error', renderedElement };
92
+ }
93
+ }
94
+ try {
95
+ switch (column.type) {
96
+ case 'component':
97
+ break;
98
+ case 'custom':
99
+ return this.formatCustomCell(cellValue, row, column);
100
+ case 'integer':
101
+ case 'number':
102
+ displayValue = cellValue.toString();
103
+ break;
104
+ case 'date':
105
+ case 'datetime':
106
+ displayValue = this.formatDateCell(cellValue, column);
107
+ break;
108
+ case 'duration':
109
+ displayValue = `${Math.floor(cellValue / 3600).toString().padStart(2, '0')}:${Math.floor((cellValue % 3600) / 60).toString().padStart(2, '0')}:${(cellValue % 60).toString().padStart(2, '0')}`;
110
+ break;
111
+ case 'string':
112
+ default:
113
+ displayValue = cellValue.toString();
114
+ break;
115
+ }
116
+ }
117
+ catch (error) {
118
+ logger.error(`Error formatting cell for column ${column.key}:`, error);
119
+ displayValue = 'Error';
120
+ }
121
+ return { display: displayValue, renderedElement };
122
+ }
123
+ transformCustomOrComponent(rawValue, column) {
124
+ if (typeof column.transformer === 'function') {
125
+ try {
126
+ return column.transformer(rawValue);
127
+ }
128
+ catch (error) {
129
+ logger.error(`Error in custom transformer function for column ${column.key}:`, error);
130
+ return 'Error';
131
+ }
132
+ }
133
+ return rawValue;
134
+ }
135
+ transformInteger(rawValue) {
136
+ return rawValue !== null ? parseInt(rawValue, 10) : null;
137
+ }
138
+ transformNumber(rawValue) {
139
+ return rawValue !== null ? parseFloat(rawValue) : null;
140
+ }
141
+ transformDate(rawValue) {
142
+ if (rawValue !== null) {
143
+ const date = new Date(rawValue);
144
+ return isNaN(date.getTime()) ? null : date.getTime();
145
+ }
146
+ return null;
147
+ }
148
+ transformDuration(rawValue) {
149
+ return rawValue !== null ? parseInt(rawValue, 10) : null;
150
+ }
151
+ transformString(rawValue) {
152
+ return rawValue !== null ? rawValue.toString() : null;
153
+ }
154
+ formatCellValue(row, column) {
155
+ var _a;
156
+ // Safely retrieve the raw value
157
+ const cellData = (_a = row[column.key]) !== null && _a !== void 0 ? _a : null;
158
+ const rawValue = cellData && typeof cellData === 'object' && 'raw' in cellData
159
+ ? cellData.raw
160
+ : cellData;
161
+ switch (column.type) {
162
+ case 'component':
163
+ case 'custom':
164
+ return this.transformCustomOrComponent(rawValue, column);
165
+ case 'integer':
166
+ return this.transformInteger(rawValue);
167
+ case 'number':
168
+ return this.transformNumber(rawValue);
169
+ case 'date':
170
+ case 'datetime':
171
+ return this.transformDate(rawValue);
172
+ case 'duration':
173
+ return this.transformDuration(rawValue);
174
+ case 'string':
175
+ default:
176
+ return this.transformString(rawValue);
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,15 @@
1
+ import type { GridService } from "../GridService";
2
+ export declare class RowHeightService {
3
+ private rowHeights;
4
+ private heightCache;
5
+ private readonly gridService;
6
+ constructor(gridService: GridService);
7
+ initRowHeights(numberOfRows: number): void;
8
+ getRowHeight(index: number): number;
9
+ getHeightCache(): number[];
10
+ updateRowHeight(index: number, height: number): void;
11
+ recalculateHeights(): void;
12
+ updateRowHeights(): void;
13
+ findStartIndex(scrollTop: number): number;
14
+ findEndIndex(scrollBottom: number): number;
15
+ }
@@ -0,0 +1,63 @@
1
+ export class RowHeightService {
2
+ constructor(gridService) {
3
+ this.rowHeights = new Map();
4
+ this.heightCache = [];
5
+ this.gridService = gridService;
6
+ }
7
+ initRowHeights(numberOfRows) {
8
+ this.rowHeights = new Map(Array.from({ length: numberOfRows }, (_, index) => [index, this.gridService.elementHeight]));
9
+ }
10
+ getRowHeight(index) {
11
+ var _a;
12
+ return (_a = this.rowHeights.get(index)) !== null && _a !== void 0 ? _a : this.gridService.elementHeight;
13
+ }
14
+ getHeightCache() {
15
+ return this.heightCache;
16
+ }
17
+ updateRowHeight(index, height) {
18
+ this.rowHeights.set(index, height);
19
+ this.recalculateHeights();
20
+ }
21
+ recalculateHeights() {
22
+ let filteredRows = this.gridService.getFilterIndexes();
23
+ this.heightCache = [];
24
+ let sum = 0;
25
+ for (let i = 0; i < this.gridService.rowCount(); i++) {
26
+ let rowIndex = filteredRows ? filteredRows[i] : i;
27
+ sum += this.getRowHeight(rowIndex);
28
+ this.heightCache.push(sum);
29
+ }
30
+ }
31
+ updateRowHeights() {
32
+ let container = this.gridService.getContainer();
33
+ if (!container)
34
+ return;
35
+ const rows = container.querySelectorAll('tbody > tr:not(.firstRow):not(.lastRow)'); // Adjust the selector as needed
36
+ let hasUpdated = false;
37
+ rows.forEach((row, index) => {
38
+ const rowHeight = row.getBoundingClientRect().height; // Get the actual row height
39
+ const rowIndex = index + this.gridService.startIndex;
40
+ if (this.getRowHeight(rowIndex) !== rowHeight) {
41
+ this.updateRowHeight(rowIndex, rowHeight);
42
+ hasUpdated = true;
43
+ }
44
+ });
45
+ if (hasUpdated) {
46
+ this.gridService.updateRenderedRows();
47
+ }
48
+ }
49
+ findStartIndex(scrollTop) {
50
+ let index = 0;
51
+ while (index < this.heightCache.length && this.heightCache[index] < scrollTop) {
52
+ index++;
53
+ }
54
+ return Math.max(0, index);
55
+ }
56
+ findEndIndex(scrollBottom) {
57
+ let index = 0;
58
+ while (index < this.heightCache.length && this.heightCache[index] < scrollBottom) {
59
+ index++;
60
+ }
61
+ return Math.min(index, this.heightCache.length - 1);
62
+ }
63
+ }
@@ -0,0 +1,14 @@
1
+ export declare class SelectionService {
2
+ private readonly selectedIndexes$;
3
+ private readonly multiSelect;
4
+ constructor(selections?: Set<number>, multiSelect?: boolean);
5
+ get selected$(): import("rxjs").Observable<Set<number>>;
6
+ selectRow(index: number): void;
7
+ deselectRow(index: number): void;
8
+ toggleRowSelection(index: number): void;
9
+ clearSelection(): void;
10
+ selectAll(indices: number[]): void;
11
+ deselectAll(): void;
12
+ shiftSelect(startIndex: number, endIndex: number): void;
13
+ isSelected(index: number): boolean;
14
+ }
@@ -0,0 +1,55 @@
1
+ import { BehaviorSubject } from 'rxjs';
2
+ export class SelectionService {
3
+ constructor(selections = new Set(), multiSelect = true) {
4
+ this.multiSelect = multiSelect;
5
+ this.selectedIndexes$ = new BehaviorSubject(selections);
6
+ }
7
+ get selected$() {
8
+ return this.selectedIndexes$.asObservable();
9
+ }
10
+ selectRow(index) {
11
+ const selected = new Set(this.selectedIndexes$.value);
12
+ selected.add(index);
13
+ this.selectedIndexes$.next(selected);
14
+ }
15
+ deselectRow(index) {
16
+ const selected = new Set(this.selectedIndexes$.value);
17
+ selected.delete(index);
18
+ this.selectedIndexes$.next(selected);
19
+ }
20
+ toggleRowSelection(index) {
21
+ const selected = new Set(this.selectedIndexes$.value);
22
+ if (selected.has(index)) {
23
+ selected.delete(index);
24
+ }
25
+ else {
26
+ if (!this.multiSelect) {
27
+ selected.clear();
28
+ }
29
+ selected.add(index);
30
+ }
31
+ this.selectedIndexes$.next(selected);
32
+ }
33
+ clearSelection() {
34
+ this.selectedIndexes$.next(new Set());
35
+ }
36
+ selectAll(indices) {
37
+ this.selectedIndexes$.next(new Set(indices));
38
+ }
39
+ deselectAll() {
40
+ this.clearSelection();
41
+ }
42
+ shiftSelect(startIndex, endIndex) {
43
+ if (!this.multiSelect)
44
+ return;
45
+ const selected = new Set(this.selectedIndexes$.value);
46
+ const [start, end] = [Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)];
47
+ for (let i = start; i <= end; i++) {
48
+ selected.add(i);
49
+ }
50
+ this.selectedIndexes$.next(selected);
51
+ }
52
+ isSelected(index) {
53
+ return this.selectedIndexes$.value.has(index);
54
+ }
55
+ }
@@ -0,0 +1,15 @@
1
+ import type { GridService } from "../GridService";
2
+ import type { Row } from "../types/grid";
3
+ import { SortingCondition } from '../types/sorting';
4
+ export declare class SortingService {
5
+ private readonly gridService;
6
+ private readonly sortConditions;
7
+ private sortedIndexes;
8
+ constructor(gridService: GridService, sortConditions?: SortingCondition[]);
9
+ getSorts(): SortingCondition[];
10
+ getSortCondition(columnKey: string | number): SortingCondition[];
11
+ toggleSort(columnKey: string | number): void;
12
+ applySorting(rows: Row[]): Row[];
13
+ getSortedIndexes(): number[] | null;
14
+ resetSorting(): void;
15
+ }
@@ -0,0 +1,82 @@
1
+ export class SortingService {
2
+ constructor(gridService, sortConditions = []) {
3
+ this.sortedIndexes = null;
4
+ this.gridService = gridService;
5
+ this.sortConditions = sortConditions;
6
+ }
7
+ getSorts() {
8
+ return this.sortConditions;
9
+ }
10
+ getSortCondition(columnKey) {
11
+ return this.sortConditions.filter(condition => columnKey === condition.columnKey);
12
+ }
13
+ toggleSort(columnKey) {
14
+ const existingIndex = this.sortConditions.findIndex(condition => condition.columnKey === columnKey);
15
+ if (existingIndex === -1) {
16
+ this.sortConditions.push({ columnKey, direction: 'asc' });
17
+ }
18
+ else {
19
+ switch (this.sortConditions[existingIndex].direction) {
20
+ case 'asc':
21
+ this.sortConditions[existingIndex].direction = 'desc';
22
+ break;
23
+ case 'desc':
24
+ this.sortConditions.splice(existingIndex, 1);
25
+ break;
26
+ }
27
+ }
28
+ this.sortedIndexes = null;
29
+ }
30
+ applySorting(rows) {
31
+ var _a;
32
+ if (this.sortConditions.length === 0) {
33
+ return rows;
34
+ }
35
+ if (this.sortedIndexes &&
36
+ this.sortedIndexes.length === rows.length &&
37
+ this.sortedIndexes.every(index => index >= 0 && index < rows.length)) {
38
+ return this.sortedIndexes.map(index => rows[index]); // Apply cached order
39
+ }
40
+ const indexedRows = rows.map((row, index) => ({ row, index }));
41
+ indexedRows.sort((a, b) => {
42
+ for (const { columnKey, direction } of this.sortConditions) {
43
+ const valueA = a.row[columnKey].value;
44
+ const valueB = b.row[columnKey].value;
45
+ let comparison = 0;
46
+ if (typeof valueA === 'string' && typeof valueB === 'string') {
47
+ comparison = valueA.localeCompare(valueB);
48
+ }
49
+ else if (typeof valueA === 'number' && typeof valueB === 'number') {
50
+ comparison = valueA - valueB;
51
+ }
52
+ else if (valueA instanceof Date && valueB instanceof Date) {
53
+ comparison = valueA.getTime() - valueB.getTime();
54
+ }
55
+ else {
56
+ comparison = String(valueA).localeCompare(String(valueB));
57
+ }
58
+ if (direction === 'desc') {
59
+ comparison = -comparison;
60
+ }
61
+ if (comparison !== 0) {
62
+ return comparison;
63
+ }
64
+ }
65
+ return 0; // Keep stable sorting
66
+ });
67
+ const filteredIndexes = (_a = this.gridService.getFilterIndexes()) !== null && _a !== void 0 ? _a : this.gridService.getRows().map((_, i) => i);
68
+ ;
69
+ this.gridService.clearIndexMap(); // Clear the existing mapping
70
+ indexedRows.forEach((indexedRow, i) => {
71
+ this.gridService.setIndexMappingValue(i, filteredIndexes[indexedRow.index]); // Map the sorted index to the original index
72
+ });
73
+ this.sortedIndexes = indexedRows.map(item => item.index);
74
+ return indexedRows.map(item => item.row); // Return sorted rows
75
+ }
76
+ getSortedIndexes() {
77
+ return this.sortedIndexes;
78
+ }
79
+ resetSorting() {
80
+ this.sortedIndexes = null;
81
+ }
82
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,140 @@
1
+ import { GridService } from '../../GridService';
2
+ describe('FilteringService', () => {
3
+ let gridService;
4
+ let filteringService;
5
+ let mockRows;
6
+ let mockColumns;
7
+ beforeEach(() => {
8
+ mockColumns = [
9
+ { key: 'id', label: 'ID', type: 'integer' },
10
+ { key: 'name', label: 'Name', type: 'string' },
11
+ { key: 'age', label: 'Age', type: 'number' },
12
+ { key: 'status', label: 'Status', type: 'string' },
13
+ ];
14
+ mockRows = [
15
+ { id: { raw: 1, value: 1 }, name: { raw: 'Alice', value: 'Alice' }, age: { raw: 30, value: 30 }, status: { raw: 'active', value: 'active' } },
16
+ { id: { raw: 2, value: 2 }, name: { raw: 'Bob', value: 'Bob' }, age: { raw: 25, value: 25 }, status: { raw: 'inactive', value: 'inactive' } },
17
+ { id: { raw: 3, value: 3 }, name: { raw: 'Charlie', value: 'Charlie' }, age: { raw: 35, value: 35 }, status: { raw: 'active', value: 'active' } },
18
+ ];
19
+ gridService = new GridService(mockRows, mockColumns);
20
+ filteringService = gridService['filteringService'];
21
+ });
22
+ describe('Basic Operations', () => {
23
+ it('should initialize with no filter', () => {
24
+ expect(filteringService.getFilterTree()).toBeNull();
25
+ expect(filteringService.getFilterIndexes()).toBeNull();
26
+ });
27
+ it('should filter by equals condition', () => {
28
+ const filter = {
29
+ type: 'condition',
30
+ columnKey: 'name',
31
+ operator: '=',
32
+ value: 'Alice'
33
+ };
34
+ filteringService.setFilterTree(filter);
35
+ expect(filteringService.getFilterIndexes()).toEqual([0]);
36
+ });
37
+ it('should filter by greater than condition', () => {
38
+ const filter = {
39
+ type: 'condition',
40
+ columnKey: 'age',
41
+ operator: '>',
42
+ value: 30
43
+ };
44
+ filteringService.setFilterTree(filter);
45
+ expect(filteringService.getFilterIndexes()).toEqual([2]);
46
+ });
47
+ });
48
+ describe('Group Filters', () => {
49
+ it('should apply AND group filter', () => {
50
+ const filter = {
51
+ type: 'group',
52
+ operator: 'and',
53
+ filters: [
54
+ { type: 'condition', columnKey: 'status', operator: '=', value: 'active' },
55
+ { type: 'condition', columnKey: 'age', operator: '>=', value: 30 }
56
+ ]
57
+ };
58
+ filteringService.setFilterTree(filter);
59
+ expect(filteringService.getFilterIndexes()).toEqual([0, 2]);
60
+ });
61
+ it('should apply OR group filter', () => {
62
+ const filter = {
63
+ type: 'group',
64
+ operator: 'or',
65
+ filters: [
66
+ { type: 'condition', columnKey: 'name', operator: '=', value: 'Alice' },
67
+ { type: 'condition', columnKey: 'name', operator: '=', value: 'Bob' }
68
+ ]
69
+ };
70
+ filteringService.setFilterTree(filter);
71
+ expect(filteringService.getFilterIndexes()).toEqual([0, 1]);
72
+ });
73
+ });
74
+ describe('Add and Reset Filters', () => {
75
+ it('should add filter and replace existing for same column', () => {
76
+ const filter1 = { type: 'condition', columnKey: 'age', operator: '>', value: 25 };
77
+ const filter2 = { type: 'condition', columnKey: 'age', operator: '<', value: 35 };
78
+ filteringService.addFilter(filter1);
79
+ filteringService.addFilter(filter2);
80
+ const tree = filteringService.getFilterTree();
81
+ expect(tree === null || tree === void 0 ? void 0 : tree.type).toBe('group');
82
+ if ((tree === null || tree === void 0 ? void 0 : tree.type) === 'group') {
83
+ expect(tree.filters.length).toBe(1);
84
+ expect(tree.filters[0]).toEqual(filter2);
85
+ }
86
+ });
87
+ it('should reset all filters', () => {
88
+ filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
89
+ filteringService.resetFilters();
90
+ expect(filteringService.getFilterTree()).toBeNull();
91
+ expect(filteringService.getFilterIndexes()).toBeNull();
92
+ });
93
+ it('should reset specific column filter', () => {
94
+ const filter = {
95
+ type: 'group',
96
+ operator: 'and',
97
+ filters: [
98
+ { type: 'condition', columnKey: 'status', operator: '=', value: 'active' },
99
+ { type: 'condition', columnKey: 'age', operator: '>', value: 25 }
100
+ ]
101
+ };
102
+ filteringService.setFilterTree(filter);
103
+ filteringService.resetFilter('status');
104
+ const tree = filteringService.getFilterTree();
105
+ if ((tree === null || tree === void 0 ? void 0 : tree.type) === 'group') {
106
+ expect(tree.filters.length).toBe(1);
107
+ expect(tree.filters[0].columnKey).toBe('age');
108
+ }
109
+ });
110
+ });
111
+ describe('Helper Methods', () => {
112
+ it('should check if column has filter', () => {
113
+ filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
114
+ expect(filteringService.hasFilter('status')).toBe(true);
115
+ expect(filteringService.hasFilter('age')).toBe(false);
116
+ });
117
+ it('should apply filter to rows', () => {
118
+ filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
119
+ const filtered = filteringService.applyFilter(mockRows);
120
+ expect(filtered.length).toBe(2);
121
+ expect(filtered[0].name.value).toBe('Alice');
122
+ expect(filtered[1].name.value).toBe('Charlie');
123
+ });
124
+ it('should combine global search with filter tree using AND semantics', () => {
125
+ filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
126
+ filteringService.setGlobalSearch('char', { caseInsensitive: true });
127
+ const filtered = filteringService.applyFilter(mockRows);
128
+ expect(filtered.length).toBe(1);
129
+ expect(filtered[0].name.value).toBe('Charlie');
130
+ });
131
+ it('should use grid case-insensitive option when global search option is not set', () => {
132
+ const caseInsensitiveGridService = new GridService(mockRows, mockColumns, { caseInsensitiveFiltering: true });
133
+ const caseInsensitiveFilteringService = caseInsensitiveGridService['filteringService'];
134
+ caseInsensitiveFilteringService.setGlobalSearch('ALICE');
135
+ const filtered = caseInsensitiveFilteringService.applyFilter(mockRows);
136
+ expect(filtered.length).toBe(1);
137
+ expect(filtered[0].name.value).toBe('Alice');
138
+ });
139
+ });
140
+ });