@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,348 @@
1
+ import { GridService } from '../GridService';
2
+ import { Column, Row } from '../types/grid';
3
+
4
+ describe('GridService', () => {
5
+ let gridService: GridService;
6
+ let mockRows: Row[];
7
+ let mockColumns: Column[];
8
+
9
+ beforeEach(() => {
10
+ // Setup mock columns
11
+ mockColumns = [
12
+ { key: 'id', label: 'ID', type: 'integer' },
13
+ { key: 'name', label: 'Name', type: 'string' },
14
+ { key: 'age', label: 'Age', type: 'number' },
15
+ ];
16
+
17
+ // Setup mock rows
18
+ mockRows = [
19
+ { id: { raw: 1, value: 1 }, name: { raw: 'Alice', value: 'Alice' }, age: { raw: 30, value: 30 } },
20
+ { id: { raw: 2, value: 2 }, name: { raw: 'Bob', value: 'Bob' }, age: { raw: 25, value: 25 } },
21
+ { id: { raw: 3, value: 3 }, name: { raw: 'Charlie', value: 'Charlie' }, age: { raw: 35, value: 35 } },
22
+ ];
23
+
24
+ gridService = new GridService(mockRows, mockColumns);
25
+ });
26
+
27
+ it('should initialize with correct row count', () => {
28
+ expect(gridService.rowCount()).toBe(3);
29
+ });
30
+
31
+ it('should return all rows', () => {
32
+ const rows = gridService.getRows();
33
+ expect(rows.length).toBe(3);
34
+ });
35
+
36
+ it('should return all columns', () => {
37
+ const columns = gridService.getColumns();
38
+ expect(columns.length).toBe(3);
39
+ });
40
+
41
+ it('should update rows', () => {
42
+ const newRows: Row[] = [
43
+ { id: { raw: 4, value: 4 }, name: { raw: 'David', value: 'David' }, age: { raw: 40, value: 40 } },
44
+ ];
45
+ gridService.setRows(newRows);
46
+ expect(gridService.rowCount()).toBe(1);
47
+ });
48
+
49
+ it('should toggle sort on a column', () => {
50
+ gridService.toggleSort('name');
51
+ const sortCondition = gridService.getSortCondition('name');
52
+ expect(sortCondition).not.toBeNull();
53
+ expect(sortCondition?.direction).toBe('asc');
54
+ });
55
+
56
+ it('should toggle sort direction from asc to desc', () => {
57
+ gridService.toggleSort('name');
58
+ gridService.toggleSort('name');
59
+ const sortCondition = gridService.getSortCondition('name');
60
+ expect(sortCondition?.direction).toBe('desc');
61
+ });
62
+
63
+ it('should remove sort after third toggle', () => {
64
+ gridService.toggleSort('name');
65
+ gridService.toggleSort('name');
66
+ gridService.toggleSort('name');
67
+ const sortCondition = gridService.getSortCondition('name');
68
+ expect(sortCondition).toBeNull();
69
+ });
70
+
71
+ it('should sort rows by age ascending', () => {
72
+ gridService.toggleSort('age');
73
+ const sortedRows = gridService.getFilteredAndSortRows();
74
+ expect(sortedRows[0].age.value).toBe(25);
75
+ expect(sortedRows[1].age.value).toBe(30);
76
+ expect(sortedRows[2].age.value).toBe(35);
77
+ });
78
+
79
+ it('should enable API mode', () => {
80
+ gridService.enableApiMode({ clientSideSort: false, clientSideFilter: false });
81
+ expect(gridService['apiMode']).toBe(true);
82
+ });
83
+
84
+ it('should set rows from API', () => {
85
+ gridService.enableApiMode({ clientSideSort: false, clientSideFilter: false });
86
+ const apiRows: Row[] = [
87
+ { id: { raw: 5, value: 5 }, name: { raw: 'Eve', value: 'Eve' }, age: { raw: 28, value: 28 } },
88
+ ];
89
+ gridService.setRowsFromApi(apiRows);
90
+ expect(gridService.rowCount()).toBe(1);
91
+ });
92
+
93
+ it('should emit filterSort$ observable when sort is toggled in API mode', (done) => {
94
+ gridService.enableApiMode({ clientSideSort: false, clientSideFilter: false });
95
+
96
+ gridService.filterSort$.subscribe(({ filterTree, sorts }) => {
97
+ if (sorts.length > 0) {
98
+ expect(sorts[0].columnKey).toBe('name');
99
+ expect(sorts[0].direction).toBe('asc');
100
+ done();
101
+ }
102
+ });
103
+
104
+ gridService.toggleSort('name');
105
+ });
106
+
107
+ it('should use client-side sorting when enabled in API mode', () => {
108
+ gridService.enableApiMode({ clientSideSort: true, clientSideFilter: false });
109
+ gridService.setRowsFromApi(mockRows);
110
+
111
+ gridService.toggleSort('age');
112
+ const sortedRows = gridService.getFilteredAndSortRows();
113
+
114
+ expect(sortedRows[0].age.value).toBe(25);
115
+ expect(sortedRows[1].age.value).toBe(30);
116
+ expect(sortedRows[2].age.value).toBe(35);
117
+ });
118
+
119
+ it('should select a row', (done) => {
120
+ gridService.selectRow(0);
121
+ gridService.getSelected().subscribe(selected => {
122
+ expect(selected.has(0)).toBe(true);
123
+ done();
124
+ });
125
+ });
126
+
127
+ it('should deselect a row', (done) => {
128
+ gridService.selectRow(0);
129
+ gridService.deselectRow(0);
130
+ gridService.getSelected().subscribe(selected => {
131
+ expect(selected.has(0)).toBe(false);
132
+ done();
133
+ });
134
+ });
135
+
136
+ it('should toggle row selection', (done) => {
137
+ gridService.toggleRowSelection(0);
138
+ gridService.getSelected().subscribe(selected => {
139
+ expect(selected.has(0)).toBe(true);
140
+ done();
141
+ });
142
+ });
143
+
144
+ it('should clear all selections', (done) => {
145
+ gridService.selectRow(0);
146
+ gridService.selectRow(1);
147
+ gridService.clearSelection();
148
+ gridService.getSelected().subscribe(selected => {
149
+ expect(selected.size).toBe(0);
150
+ done();
151
+ });
152
+ });
153
+
154
+ it('should update display format', (done) => {
155
+ gridService.updateDisplayFormat({ dateFormat: 'dd/MM/yyyy' });
156
+ gridService.displayFormat$.subscribe(format => {
157
+ expect(format.dateFormat).toBe('dd/MM/yyyy');
158
+ done();
159
+ });
160
+ });
161
+
162
+ it('should set and clear global search', () => {
163
+ gridService.setGlobalSearch('Alice');
164
+ expect(gridService.getGlobalSearch()).toEqual({ query: 'Alice', options: undefined });
165
+
166
+ const searchedRows = gridService.getFilteredRows();
167
+ expect(searchedRows).toHaveLength(1);
168
+ expect(searchedRows[0].name.value).toBe('Alice');
169
+
170
+ gridService.clearGlobalSearch();
171
+ expect(gridService.getGlobalSearch()).toBeNull();
172
+ expect(gridService.getFilteredRows()).toHaveLength(3);
173
+ });
174
+
175
+ it('should apply global search only to restricted columns', () => {
176
+ gridService.setGlobalSearch('25', { columns: ['name'] });
177
+ expect(gridService.getFilteredRows()).toHaveLength(0);
178
+
179
+ gridService.setGlobalSearch('25', { columns: ['age'] });
180
+ const searchedRows = gridService.getFilteredRows();
181
+ expect(searchedRows).toHaveLength(1);
182
+ expect(searchedRows[0].name.value).toBe('Bob');
183
+ });
184
+
185
+ it('should apply case-insensitive global search using options', () => {
186
+ gridService.setGlobalSearch('ALICE', { caseInsensitive: true });
187
+ const searchedRows = gridService.getFilteredRows();
188
+
189
+ expect(searchedRows).toHaveLength(1);
190
+ expect(searchedRows[0].name.value).toBe('Alice');
191
+ });
192
+
193
+ it('should update global search matches after setRows', () => {
194
+ gridService.setGlobalSearch('zara', { caseInsensitive: true });
195
+ expect(gridService.getFilteredRows()).toHaveLength(0);
196
+
197
+ const newRows: Row[] = [
198
+ { id: { raw: 10, value: 10 }, name: { raw: 'Zara', value: 'Zara' }, age: { raw: 22, value: 22 } },
199
+ ];
200
+
201
+ gridService.setRows(newRows);
202
+ const searchedRows = gridService.getFilteredRows();
203
+ expect(searchedRows).toHaveLength(1);
204
+ expect(searchedRows[0].name.value).toBe('Zara');
205
+ });
206
+
207
+ it('should update global search matches after setColumns', () => {
208
+ gridService.setGlobalSearch('30', { columns: ['age'] });
209
+ expect(gridService.getFilteredRows()).toHaveLength(1);
210
+
211
+ gridService.setColumns([
212
+ { key: 'id', label: 'ID', type: 'integer' },
213
+ { key: 'name', label: 'Name', type: 'string' },
214
+ ]);
215
+
216
+ gridService.setGlobalSearch('30');
217
+ expect(gridService.getFilteredRows()).toHaveLength(0);
218
+ });
219
+
220
+ it('should emit global search in filterSort$ payload in API mode', (done) => {
221
+ gridService.enableApiMode({ clientSideSort: false, clientSideFilter: false });
222
+
223
+ gridService.filterSort$.subscribe(({ globalSearch }) => {
224
+ if (globalSearch?.query === 'alice') {
225
+ expect(globalSearch.options?.columns).toEqual(['name']);
226
+ expect(globalSearch.options?.caseInsensitive).toBe(true);
227
+ done();
228
+ }
229
+ });
230
+
231
+ gridService.setGlobalSearch('alice', { columns: ['name'], caseInsensitive: true });
232
+ });
233
+
234
+ it('should support display-mode global search', () => {
235
+ const durationColumns: Column[] = [
236
+ { key: 'name', label: 'Name', type: 'string' },
237
+ { key: 'sessionDuration', label: 'Session Duration', type: 'duration' },
238
+ ];
239
+
240
+ const durationRows: Row[] = [
241
+ { name: { raw: 'Alice', value: 'Alice' }, sessionDuration: { raw: 3661, value: 3661 } },
242
+ { name: { raw: 'Bob', value: 'Bob' }, sessionDuration: { raw: 125, value: 125 } },
243
+ ];
244
+
245
+ const durationGrid = new GridService(durationRows, durationColumns);
246
+
247
+ durationGrid.setGlobalSearch('01:01', { columns: ['sessionDuration'], searchMode: 'display' });
248
+ const displayMatches = durationGrid.getFilteredRows();
249
+ expect(displayMatches).toHaveLength(1);
250
+ expect(displayMatches[0].name.value).toBe('Alice');
251
+
252
+ durationGrid.setGlobalSearch('01:01', { columns: ['sessionDuration'], searchMode: 'raw' });
253
+ expect(durationGrid.getFilteredRows()).toHaveLength(0);
254
+ });
255
+
256
+ it('should refresh display-mode global search after display format updates', () => {
257
+ const dateColumns: Column[] = [
258
+ { key: 'name', label: 'Name', type: 'string' },
259
+ { key: 'created', label: 'Created', type: 'date' },
260
+ ];
261
+
262
+ const dateRows: Row[] = [
263
+ { name: { raw: 'Alice', value: 'Alice' }, created: { raw: '2024-01-15T00:00:00Z', value: new Date('2024-01-15T00:00:00Z').getTime() } },
264
+ { name: { raw: 'Bob', value: 'Bob' }, created: { raw: '2024-12-31T00:00:00Z', value: new Date('2024-12-31T00:00:00Z').getTime() } },
265
+ ];
266
+
267
+ const dateGrid = new GridService(dateRows, dateColumns);
268
+
269
+ dateGrid.setGlobalSearch('/', { columns: ['created'], searchMode: 'display' });
270
+ expect(dateGrid.getFilteredRows()).toHaveLength(0);
271
+
272
+ dateGrid.updateDisplayFormat({ dateFormat: 'dd/MM/yyyy' });
273
+ expect(dateGrid.getFilteredRows()).toHaveLength(2);
274
+ });
275
+
276
+ it('should use column globalSearchText callback for component columns', () => {
277
+ const componentColumns: Column[] = [
278
+ { key: 'id', label: 'ID', type: 'integer' },
279
+ {
280
+ key: 'status',
281
+ label: 'Status',
282
+ type: 'component',
283
+ globalSearchText: (cell) => {
284
+ const code = String(cell?.raw ?? '');
285
+ if (code === 'A') return 'Active';
286
+ if (code === 'I') return 'Inactive';
287
+ return 'Pending';
288
+ },
289
+ },
290
+ ];
291
+
292
+ const componentRows: Row[] = [
293
+ { id: { raw: 1, value: 1 }, status: { raw: 'A', value: 'A' } },
294
+ { id: { raw: 2, value: 2 }, status: { raw: 'I', value: 'I' } },
295
+ ];
296
+
297
+ const componentGrid = new GridService(componentRows, componentColumns);
298
+ componentGrid.setGlobalSearch('inactive', { searchMode: 'display', caseInsensitive: true });
299
+
300
+ const searchedRows = componentGrid.getFilteredRows();
301
+ expect(searchedRows).toHaveLength(1);
302
+ expect(searchedRows[0].id.value).toBe(2);
303
+ });
304
+
305
+ it('should keep sort consistent when global search query changes', () => {
306
+ gridService.toggleSort('age');
307
+ expect(gridService.getSortCondition('age')?.direction).toBe('asc');
308
+
309
+ gridService.setGlobalSearch('a', { caseInsensitive: true });
310
+ expect(gridService.getSortCondition('age')?.direction).toBe('asc');
311
+ const firstSearch = gridService.getFilteredAndSortRows();
312
+ expect(firstSearch).toHaveLength(2);
313
+ expect(firstSearch[0].name.value).toBe('Alice');
314
+ expect(firstSearch[1].name.value).toBe('Charlie');
315
+
316
+ gridService.setGlobalSearch('bo', { caseInsensitive: true });
317
+ expect(gridService.getSortCondition('age')?.direction).toBe('asc');
318
+ const secondSearch = gridService.getFilteredAndSortRows();
319
+ expect(secondSearch).toHaveLength(1);
320
+ expect(secondSearch[0].name.value).toBe('Bob');
321
+ });
322
+
323
+ it('should combine global search and per-column filter with sorting', () => {
324
+ gridService.toggleSort('age');
325
+
326
+ gridService.addFilter({ type: 'condition', columnKey: 'age', operator: '>=', value: 30 });
327
+ gridService.setGlobalSearch('a', { caseInsensitive: true });
328
+
329
+ const rows = gridService.getFilteredAndSortRows();
330
+ expect(rows).toHaveLength(2);
331
+ expect(rows[0].name.value).toBe('Alice');
332
+ expect(rows[1].name.value).toBe('Charlie');
333
+ });
334
+
335
+ it('should not apply global search locally in API mode when client-side filtering is disabled', () => {
336
+ gridService.enableApiMode({ clientSideSort: true, clientSideFilter: false });
337
+ gridService.setRowsFromApi(mockRows);
338
+
339
+ gridService.setGlobalSearch('Alice');
340
+ gridService.toggleSort('age');
341
+
342
+ const rows = gridService.getFilteredAndSortRows();
343
+ expect(rows).toHaveLength(3);
344
+ expect(rows[0].name.value).toBe('Bob');
345
+ expect(rows[1].name.value).toBe('Alice');
346
+ expect(rows[2].name.value).toBe('Charlie');
347
+ });
348
+ });
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { GridService } from './GridService';
2
+ export {
3
+ GridState,
4
+ Row,
5
+ RenderRow,
6
+ Column,
7
+ ColumnType,
8
+ DisplayFormat,
9
+ GridOptions,
10
+ FilterOption,
11
+ GlobalSearchMode,
12
+ GlobalSearchOptions,
13
+ GlobalSearchState,
14
+ FilterSortUpdatePayload,
15
+ } from './types/grid';
16
+ export { SortingCondition } from './types/sorting';
17
+ export { FilterCondition, OperatorType, FilterGroup, FilterNode } from '@open-dcp/filter';