@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,410 @@
1
+ import { BehaviorSubject, skip, combineLatest, map } from 'rxjs';
2
+ import { isEqual } from 'lodash-es';
3
+ import { distinctUntilChanged } from 'rxjs/operators';
4
+ import { logger } from '@open-dcp/utilities';
5
+ import { RowHeightService } from './services/RowHeightService';
6
+ import { SortingService } from './services/SortingService';
7
+ import { FormattingService } from './services/FormattingService';
8
+ import { FilteringService } from './services/FilteringService';
9
+ import { SelectionService } from './services/SelectionService';
10
+ const DEFAULT_DISPLAY_FORMAT = {
11
+ timeZone: "UTC",
12
+ dateFormat: "yyyy-MM-dd",
13
+ timeFormat: "HH:mm:ss",
14
+ };
15
+ export class GridService {
16
+ constructor(rows, columns, gridOptions = {}, sorts = [], filterTree = null, selections = new Set()) {
17
+ var _a, _b;
18
+ this.container = null; // Reference to the scrolling container
19
+ this.startIndex = 0;
20
+ this.firstRowHeight = 0;
21
+ this.lastRowHeight = 0;
22
+ this.apiMode = false;
23
+ this.clientSideSort = false;
24
+ this.clientSideFilter = false;
25
+ this.indexMapping = new Map();
26
+ this.gridOptions = gridOptions;
27
+ this.filterSortSubject = new BehaviorSubject({ filterTree, sorts, globalSearch: null });
28
+ this.filterSort$ = this.filterSortSubject.asObservable();
29
+ this.displayFormatSubject = new BehaviorSubject(Object.assign(Object.assign({}, DEFAULT_DISPLAY_FORMAT), gridOptions.displayFormat));
30
+ this.displayFormat$ = this.displayFormatSubject.asObservable();
31
+ // Data independend services
32
+ this.formattingService = new FormattingService(this);
33
+ this.rowHeightService = new RowHeightService(this);
34
+ // Set Column
35
+ this.columnsSubject = new BehaviorSubject(columns);
36
+ this.columns$ = this.columnsSubject.asObservable();
37
+ // Format Rows
38
+ let initalRows = rows.map(row => this.formattingService.applyFormattingToRow(row));
39
+ this.rowsSubject = new BehaviorSubject(initalRows);
40
+ this.rows$ = this.rowsSubject.asObservable();
41
+ this.indexMapping = new Map([...Array(initalRows.length).keys()].map(i => [i, i]));
42
+ // Row Count Observable
43
+ this.rowCount$ = combineLatest([
44
+ this.rows$,
45
+ this.filterSort$,
46
+ this.columns$
47
+ ]).pipe(map(() => this.rowCount()), distinctUntilChanged());
48
+ // Init Data Services
49
+ this.filteringService = new FilteringService(this, filterTree);
50
+ this.sortingService = new SortingService(this, sorts);
51
+ this.selectionService = new SelectionService(selections, (_a = gridOptions.multiSelect) !== null && _a !== void 0 ? _a : true);
52
+ this.elementHeight = (_b = gridOptions.elementHeight) !== null && _b !== void 0 ? _b : 42;
53
+ this.stateSubject = new BehaviorSubject({
54
+ startIndex: 0,
55
+ step: 1,
56
+ firstRowHeight: 0,
57
+ lastRowHeight: 0,
58
+ renderRows: []
59
+ });
60
+ this.rows$.pipe(skip(1), distinctUntilChanged((prev, curr) => isEqual(curr, prev))).subscribe(() => {
61
+ if (this.apiMode) {
62
+ return;
63
+ }
64
+ this.updateRenderedRows();
65
+ this.rowHeightService.initRowHeights(this.rowCount());
66
+ });
67
+ this.columns$.pipe(distinctUntilChanged((prev, curr) => isEqual(curr, prev))).subscribe(() => {
68
+ if (this.getRows().length > 0) {
69
+ this.setRows(this.rowsSubject.getValue());
70
+ }
71
+ });
72
+ this.displayFormat$.subscribe(() => {
73
+ this.updateRenderedRows();
74
+ });
75
+ }
76
+ // API Support
77
+ notifyFilterSortUpdate() {
78
+ const filterTree = this.filteringService.getFilterTree();
79
+ const sorts = this.sortingService.getSorts();
80
+ const globalSearch = this.filteringService.getGlobalSearch();
81
+ // Always emit to observable
82
+ this.filterSortSubject.next({
83
+ filterTree,
84
+ sorts,
85
+ globalSearch,
86
+ });
87
+ if (this.apiMode && !this.clientSideSort && !this.clientSideFilter) {
88
+ // In API mode or with client-side operations, do not update locally
89
+ return;
90
+ }
91
+ this.updateRenderedRows();
92
+ }
93
+ enableApiMode(config = {}) {
94
+ var _a, _b;
95
+ logger.log('DcpGrid: api mode active');
96
+ this.apiMode = true;
97
+ this.clientSideSort = (_a = config.clientSideSort) !== null && _a !== void 0 ? _a : false;
98
+ this.clientSideFilter = (_b = config.clientSideFilter) !== null && _b !== void 0 ? _b : false;
99
+ }
100
+ disableApiMode() {
101
+ this.apiMode = false;
102
+ this.clientSideSort = false;
103
+ this.clientSideFilter = false;
104
+ }
105
+ shouldApplyClientSideFiltering() {
106
+ return !this.apiMode || this.clientSideFilter;
107
+ }
108
+ setRowsFromApi(rows) {
109
+ if (!this.apiMode) {
110
+ logger.warn('DcpGrid: setRowsFromApi should only be called in API mode');
111
+ return;
112
+ }
113
+ // Format the rows first!
114
+ const formattedRows = rows.map(row => this.formattingService.applyFormattingToRow(row));
115
+ // Set the formatted rows
116
+ this.rowsSubject.next(formattedRows);
117
+ // Reset index mapping - it will be recalculated based on client-side operations
118
+ this.indexMapping = new Map([...Array(formattedRows.length).keys()].map(i => [i, i]));
119
+ this.rowHeightService.initRowHeights(formattedRows.length);
120
+ this.updateRenderedRows();
121
+ }
122
+ // Index Mapping
123
+ clearIndexMap() {
124
+ this.indexMapping.clear();
125
+ }
126
+ setIndexMapping(newMap) {
127
+ this.indexMapping = newMap;
128
+ }
129
+ setIndexMappingValue(newIndex, originalIndex) {
130
+ this.indexMapping.set(newIndex, originalIndex);
131
+ }
132
+ getOriginalIndex(index) {
133
+ const originalIndex = this.indexMapping.get(index);
134
+ if (originalIndex === undefined) {
135
+ throw new Error(`Original index in rows not found for: ${index}`);
136
+ }
137
+ return originalIndex;
138
+ }
139
+ getIndexMapping() {
140
+ return this.indexMapping;
141
+ }
142
+ // Others
143
+ updateDisplayFormat(newFormat) {
144
+ var _a;
145
+ this.displayFormatSubject.next(Object.assign(Object.assign({}, this.displayFormatSubject.getValue()), newFormat));
146
+ const globalSearch = this.filteringService.getGlobalSearch();
147
+ if (((_a = globalSearch === null || globalSearch === void 0 ? void 0 : globalSearch.options) === null || _a === void 0 ? void 0 : _a.searchMode) === 'display') {
148
+ this.filteringService.updateFilterIndexesForUpdatedData();
149
+ }
150
+ }
151
+ attachContainer(container) {
152
+ this.container = container;
153
+ this.rowHeightService.recalculateHeights();
154
+ this.updateRenderedRows();
155
+ }
156
+ getContainer() {
157
+ return this.container;
158
+ }
159
+ setStateValue(key, newValue) {
160
+ this.stateSubject.next(Object.assign(Object.assign({}, this.stateSubject.value), { [key]: newValue }));
161
+ }
162
+ getState() {
163
+ return this.stateSubject.asObservable();
164
+ }
165
+ getFirstRowHeight() {
166
+ return this.firstRowHeight;
167
+ }
168
+ getLastRowHeight() {
169
+ return this.lastRowHeight;
170
+ }
171
+ getRenderRows() {
172
+ return this.stateSubject.getValue().renderRows;
173
+ }
174
+ getRows() {
175
+ return this.rowsSubject.getValue();
176
+ }
177
+ getCellDisplayValue(row, columnKey) {
178
+ const column = this.columnsSubject.getValue().find((col) => col.key === columnKey);
179
+ if (!column) {
180
+ return '';
181
+ }
182
+ const displayValue = this.formattingService.formatCellDisplay(row, column).display;
183
+ return displayValue === null || displayValue === undefined ? '' : String(displayValue);
184
+ }
185
+ getFilteredRows() {
186
+ if (this.apiMode && !this.clientSideFilter) {
187
+ // In API mode without client-side filtering, return rows as-is (already filtered by backend)
188
+ return this.getRows();
189
+ }
190
+ // Apply client-side filtering
191
+ return this.filteringService.applyFilter(this.getRows());
192
+ }
193
+ getFilteredAndSortRows() {
194
+ const filteredRows = this.getFilteredRows();
195
+ if (this.apiMode && !this.clientSideSort) {
196
+ // In API mode without client-side sorting, return rows as-is (already sorted by backend)
197
+ return filteredRows;
198
+ }
199
+ // Apply client-side sorting
200
+ return this.sortingService.applySorting(filteredRows);
201
+ }
202
+ rowCount() {
203
+ let totalRows = this.rowsSubject.getValue().length;
204
+ if (this.apiMode && !this.clientSideFilter) {
205
+ // In API mode without client-side filtering, use total rows
206
+ return totalRows;
207
+ }
208
+ // Use filtered count
209
+ let filterIndexes = this.filteringService.getFilterIndexes();
210
+ if (filterIndexes === null) {
211
+ return totalRows;
212
+ }
213
+ return filterIndexes.length;
214
+ }
215
+ setRows(rows, shouldFormat = true) {
216
+ const updatedRows = shouldFormat
217
+ ? rows.map(row => this.formattingService.applyFormattingToRow(row))
218
+ : rows;
219
+ this.indexMapping = new Map([...Array(rows.length).keys()].map(i => [i, i]));
220
+ this.rowsSubject.next(updatedRows);
221
+ this.filteringService.updateFilterIndexesForUpdatedData();
222
+ }
223
+ getColumns() {
224
+ return this.columnsSubject.getValue();
225
+ }
226
+ setColumns(columns) {
227
+ this.columnsSubject.next(columns);
228
+ }
229
+ // SortingService
230
+ getSortCondition(columnKey) {
231
+ let matchingConditions = this.sortingService.getSortCondition(columnKey);
232
+ if (matchingConditions.length == 0) {
233
+ return null;
234
+ }
235
+ return matchingConditions[0];
236
+ }
237
+ toggleSort(columnKey) {
238
+ this.sortingService.toggleSort(columnKey);
239
+ this.notifyFilterSortUpdate();
240
+ }
241
+ // FilteringService
242
+ addFilter(filterNode) {
243
+ this.filteringService.addFilter(filterNode);
244
+ this.sortingService.resetSorting();
245
+ this.notifyFilterSortUpdate();
246
+ }
247
+ reset() {
248
+ this.filteringService.resetFilters();
249
+ this.sortingService.resetSorting();
250
+ this.notifyFilterSortUpdate();
251
+ }
252
+ resetFilter(columnKey) {
253
+ this.filteringService.resetFilter(columnKey);
254
+ this.sortingService.resetSorting();
255
+ this.notifyFilterSortUpdate();
256
+ }
257
+ setGlobalSearch(query, options) {
258
+ this.filteringService.setGlobalSearch(query, options);
259
+ this.notifyFilterSortUpdate();
260
+ }
261
+ clearGlobalSearch() {
262
+ this.filteringService.clearGlobalSearch();
263
+ this.notifyFilterSortUpdate();
264
+ }
265
+ getGlobalSearch() {
266
+ return this.filteringService.getGlobalSearch();
267
+ }
268
+ getFilterIndexes() {
269
+ return this.filteringService.getFilterIndexes();
270
+ }
271
+ hasFilter(columnKey) {
272
+ return this.filteringService.hasFilter(columnKey);
273
+ }
274
+ // SelectionService
275
+ getSelected() {
276
+ return this.selectionService.selected$;
277
+ }
278
+ selectRow(index) {
279
+ this.selectionService.selectRow(index);
280
+ }
281
+ deselectRow(index) {
282
+ this.selectionService.deselectRow(index);
283
+ }
284
+ toggleRowSelection(index) {
285
+ this.selectionService.toggleRowSelection(index);
286
+ }
287
+ selectAll() {
288
+ const rowsOfInterest = this.getRowsOfInterest();
289
+ const indices = rowsOfInterest.map((_, i) => i);
290
+ this.selectionService.selectAll(indices);
291
+ }
292
+ shiftSelect(startIndex, endIndex) {
293
+ this.selectionService.shiftSelect(startIndex, endIndex);
294
+ }
295
+ isSelected(index) {
296
+ return this.selectionService.isSelected(index);
297
+ }
298
+ deselectAll() {
299
+ this.selectionService.deselectAll();
300
+ }
301
+ clearSelection() {
302
+ this.selectionService.clearSelection();
303
+ }
304
+ getDisplayValue(row, column) {
305
+ return this.formattingService.formatCellDisplay(row, column);
306
+ }
307
+ getRowsOfInterest() {
308
+ if (this.apiMode) {
309
+ // API mode: apply only client-side operations
310
+ if (this.clientSideFilter && this.clientSideSort) {
311
+ return this.getFilteredAndSortRows();
312
+ }
313
+ else if (this.clientSideFilter) {
314
+ return this.getFilteredRows();
315
+ }
316
+ else if (this.clientSideSort) {
317
+ return this.sortingService.applySorting(this.getRows());
318
+ }
319
+ else {
320
+ // Pure API mode - no client-side operations
321
+ return this.getRows();
322
+ }
323
+ }
324
+ else {
325
+ // Normal mode - always apply both
326
+ return this.getFilteredAndSortRows();
327
+ }
328
+ }
329
+ rebuildIndexMapping(processedRows) {
330
+ const originalRows = this.getRows();
331
+ const newMapping = new Map();
332
+ processedRows.forEach((processedRow, newIdx) => {
333
+ const originalIdx = originalRows.indexOf(processedRow);
334
+ if (originalIdx !== -1) {
335
+ newMapping.set(newIdx, originalIdx);
336
+ }
337
+ });
338
+ this.indexMapping = newMapping;
339
+ }
340
+ updateRenderedRows() {
341
+ var _a;
342
+ if (!this.container)
343
+ return;
344
+ const scrollTop = (_a = this.getScrollHeight()) !== null && _a !== void 0 ? _a : 0;
345
+ // Get the appropriate rows based on mode and settings
346
+ const rowsOfInterest = this.getRowsOfInterest();
347
+ // Rebuild index mapping ONLY if client-side operations were applied in API mode
348
+ if (this.apiMode && (this.clientSideFilter || this.clientSideSort)) {
349
+ this.rebuildIndexMapping(rowsOfInterest);
350
+ }
351
+ else if (this.apiMode && !this.clientSideFilter && !this.clientSideSort) {
352
+ // Pure API mode - ensure 1:1 mapping
353
+ const rows = this.getRows();
354
+ this.indexMapping = new Map([...Array(rows.length).keys()].map(i => [i, i]));
355
+ }
356
+ this.rowHeightService.recalculateHeights();
357
+ const scrollBottom = scrollTop + this.container.clientHeight;
358
+ this.startIndex = this.rowHeightService.findStartIndex(scrollTop);
359
+ const endIndex = this.rowHeightService.findEndIndex(scrollBottom);
360
+ // Correct firstRowHeight using cumulative heights
361
+ this.firstRowHeight = this.startIndex > 0 && rowsOfInterest.length > 0 ? this.rowHeightService.getHeightCache()[this.startIndex - 1] : 0;
362
+ // Calculate the actual visible height
363
+ const totalHeight = this.rowHeightService.getHeightCache()[this.rowHeightService.getHeightCache().length - 1] || 0;
364
+ const visibleHeight = this.rowHeightService.getHeightCache()[endIndex] - this.firstRowHeight;
365
+ // Ensure lastRowHeight is computed dynamically
366
+ this.lastRowHeight = rowsOfInterest.length > 0 ? Math.max(0, totalHeight - (this.firstRowHeight + visibleHeight)) : 0;
367
+ // Slice visible rows
368
+ const visibleRows = rowsOfInterest.slice(this.startIndex, endIndex + 1);
369
+ // Apply formatting
370
+ const formattedRenderRows = visibleRows.map((row, index) => {
371
+ var _a;
372
+ const currentIndex = this.startIndex + index;
373
+ // In pure API mode (no client-side operations), use currentIndex directly
374
+ const originalIndex = (this.apiMode && !this.clientSideFilter && !this.clientSideSort)
375
+ ? currentIndex
376
+ : (_a = this.indexMapping.get(currentIndex)) !== null && _a !== void 0 ? _a : currentIndex;
377
+ const formattedRow = {
378
+ globaIndex: originalIndex,
379
+ data: row
380
+ };
381
+ this.columnsSubject.getValue().forEach((column) => {
382
+ const cellValue = formattedRow.data[column.key];
383
+ if (cellValue && typeof cellValue === 'object' && cellValue.raw !== undefined) {
384
+ const displayValue = this.getDisplayValue(row, column);
385
+ formattedRow.data[column.key].display = displayValue.display;
386
+ formattedRow.data[column.key].renderedElement = displayValue.renderedElement;
387
+ }
388
+ });
389
+ return formattedRow;
390
+ });
391
+ // Update state
392
+ this.stateSubject.next({
393
+ startIndex: this.startIndex,
394
+ step: endIndex - this.startIndex,
395
+ firstRowHeight: this.firstRowHeight,
396
+ lastRowHeight: this.lastRowHeight,
397
+ renderRows: formattedRenderRows
398
+ });
399
+ requestAnimationFrame(() => this.rowHeightService.updateRowHeights());
400
+ }
401
+ handleScroll(event) {
402
+ if (!this.container)
403
+ return;
404
+ this.updateRenderedRows();
405
+ }
406
+ getScrollHeight() {
407
+ var _a;
408
+ return (_a = this.container) === null || _a === void 0 ? void 0 : _a.scrollTop;
409
+ }
410
+ }
@@ -0,0 +1 @@
1
+ export {};