@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.
- package/.github/workflows/main.yml +42 -0
- package/CLA.md +22 -0
- package/LICENSE +190 -0
- package/NOTICE.md +31 -0
- package/README.md +39 -3
- package/SECURITY.md +27 -0
- package/dist/GridService.d.ts +83 -0
- package/dist/GridService.js +410 -0
- package/dist/__tests__/GridServices.test.d.ts +1 -0
- package/dist/__tests__/GridServices.test.js +288 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/services/FilteringService.d.ts +34 -0
- package/dist/services/FilteringService.js +278 -0
- package/dist/services/FormattingService.d.ts +17 -0
- package/dist/services/FormattingService.js +179 -0
- package/dist/services/RowHeightService.d.ts +15 -0
- package/dist/services/RowHeightService.js +63 -0
- package/dist/services/SelectionService.d.ts +14 -0
- package/dist/services/SelectionService.js +55 -0
- package/dist/services/SortingService.d.ts +15 -0
- package/dist/services/SortingService.js +82 -0
- package/dist/services/__tests__/FilteringService.test.d.ts +1 -0
- package/dist/services/__tests__/FilteringService.test.js +140 -0
- package/dist/services/__tests__/FormattingService.test.d.ts +1 -0
- package/dist/services/__tests__/FormattingService.test.js +122 -0
- package/dist/types/grid.d.ts +71 -0
- package/dist/types/grid.js +1 -0
- package/dist/types/sorting.d.ts +8 -0
- package/dist/types/sorting.js +1 -0
- package/jest.config.js +16 -0
- package/package.json +43 -4
- package/reports/junit.xml +3 -0
- package/src/GridService.ts +561 -0
- package/src/__tests__/GridServices.test.ts +348 -0
- package/src/index.ts +17 -0
- package/src/services/FilteringService.ts +347 -0
- package/src/services/FormattingService.ts +205 -0
- package/src/services/RowHeightService.ts +81 -0
- package/src/services/SelectionService.ts +71 -0
- package/src/services/SortingService.ts +105 -0
- package/src/services/__tests__/FilteringService.test.ts +173 -0
- package/src/services/__tests__/FormattingService.test.ts +157 -0
- package/src/types/grid.ts +84 -0
- package/src/types/sorting.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
import type { GridService } from "../GridService";
|
|
3
|
+
import type { CellValue, Column, GlobalSearchMode, GlobalSearchOptions, GlobalSearchState, Row } from "../types/grid";
|
|
4
|
+
import { FilterCondition, FilterNode, evaluateFilterTree } from "@open-dcp/filter";
|
|
5
|
+
import { logger } from '@open-dcp/utilities';
|
|
6
|
+
|
|
7
|
+
export class FilteringService {
|
|
8
|
+
private readonly gridService: GridService;
|
|
9
|
+
private readonly filteredIndexes$: BehaviorSubject<number[] | null> = new BehaviorSubject<number[] | null>(null);
|
|
10
|
+
private filterTree: FilterNode | null = null;
|
|
11
|
+
private globalSearch: GlobalSearchState | null = null;
|
|
12
|
+
private searchableTextCache: string[] | null = null;
|
|
13
|
+
private cacheSignature: string | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(gridService: GridService, filterTree: FilterNode | null = null) {
|
|
16
|
+
this.gridService = gridService;
|
|
17
|
+
this.filterTree = filterTree;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private getColumnKeys(node: FilterNode): (string | number)[] {
|
|
21
|
+
if (node.type === 'condition') return [node.columnKey];
|
|
22
|
+
if (node.type === 'group') {
|
|
23
|
+
return node.filters.reduce<(string | number)[]>((acc, filter) => {
|
|
24
|
+
return acc.concat(this.getColumnKeys(filter));
|
|
25
|
+
}, []);
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setFilterTree(tree: FilterNode | null) {
|
|
31
|
+
this.filterTree = tree;
|
|
32
|
+
this.updateFilterIndexes();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getFilterTree(): FilterNode | null {
|
|
36
|
+
return this.filterTree;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setGlobalSearch(query: string, options?: GlobalSearchOptions): void {
|
|
40
|
+
const normalizedQuery = query.trim();
|
|
41
|
+
|
|
42
|
+
if (normalizedQuery.length === 0) {
|
|
43
|
+
this.globalSearch = null;
|
|
44
|
+
this.updateFilterIndexes();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.globalSearch = {
|
|
49
|
+
query: normalizedQuery,
|
|
50
|
+
options: options ? { ...options } : undefined,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.updateFilterIndexes();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
clearGlobalSearch(): void {
|
|
57
|
+
this.globalSearch = null;
|
|
58
|
+
this.updateFilterIndexes();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getGlobalSearch(): GlobalSearchState | null {
|
|
62
|
+
return this.globalSearch;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
updateFilterIndexesForUpdatedData() {
|
|
66
|
+
this.invalidateSearchCache();
|
|
67
|
+
this.updateFilterIndexes();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private invalidateSearchCache(): void {
|
|
71
|
+
this.searchableTextCache = null;
|
|
72
|
+
this.cacheSignature = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private getSearchColumns(options?: GlobalSearchOptions): Column[] {
|
|
76
|
+
const allColumns = this.gridService.getColumns();
|
|
77
|
+
|
|
78
|
+
if (options?.columns && options.columns.length > 0) {
|
|
79
|
+
const keySet = new Set(options.columns);
|
|
80
|
+
const filteredColumns = allColumns.filter((column) => keySet.has(column.key));
|
|
81
|
+
|
|
82
|
+
if (filteredColumns.length === 0) {
|
|
83
|
+
logger.warn('DcpGrid: Global search columns do not match any configured columns');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return filteredColumns;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return allColumns;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private toSearchValue(value: unknown): string {
|
|
93
|
+
if (value === null || value === undefined) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (value instanceof Date) {
|
|
98
|
+
return value.toISOString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return String(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private getCellSearchValue(row: Row, column: Column, searchMode: GlobalSearchMode): string {
|
|
105
|
+
const columnKey = column.key;
|
|
106
|
+
const cell = row[columnKey];
|
|
107
|
+
const mappedCell = (cell && typeof cell === 'object') ? (cell as unknown as Record<string, unknown>) : null;
|
|
108
|
+
|
|
109
|
+
if (typeof column.globalSearchText === 'function') {
|
|
110
|
+
const customValue = column.globalSearchText(mappedCell as CellValue | null, row, searchMode);
|
|
111
|
+
const customText = this.toSearchValue(customValue);
|
|
112
|
+
if (customText.length > 0) {
|
|
113
|
+
return customText;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (mappedCell) {
|
|
118
|
+
|
|
119
|
+
if (searchMode === 'raw' && 'raw' in mappedCell) {
|
|
120
|
+
return this.toSearchValue(mappedCell.raw);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (searchMode === 'display') {
|
|
124
|
+
if ('display' in mappedCell) {
|
|
125
|
+
const displayText = this.toSearchValue(mappedCell.display);
|
|
126
|
+
if (displayText.length > 0) {
|
|
127
|
+
return displayText;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const formattedDisplay = this.gridService.getCellDisplayValue(row, columnKey);
|
|
132
|
+
if (formattedDisplay.length > 0) {
|
|
133
|
+
return formattedDisplay;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ('value' in mappedCell) {
|
|
137
|
+
return this.toSearchValue(mappedCell.value);
|
|
138
|
+
}
|
|
139
|
+
if ('raw' in mappedCell) {
|
|
140
|
+
return this.toSearchValue(mappedCell.raw);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ('value' in mappedCell) {
|
|
145
|
+
return this.toSearchValue(mappedCell.value);
|
|
146
|
+
}
|
|
147
|
+
if ('raw' in mappedCell) {
|
|
148
|
+
return this.toSearchValue(mappedCell.raw);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return this.toSearchValue(cell);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private getCacheSignature(columns: Column[], caseInsensitive: boolean, searchMode: GlobalSearchMode): string {
|
|
156
|
+
const columnKeys = columns.map((column) => String(column.key));
|
|
157
|
+
return `${columnKeys.join('|')}::${caseInsensitive ? 'i' : 's'}::${searchMode}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private ensureSearchCache(rows: Row[], columns: Column[], caseInsensitive: boolean, searchMode: GlobalSearchMode): void {
|
|
161
|
+
const signature = this.getCacheSignature(columns, caseInsensitive, searchMode);
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
this.searchableTextCache !== null &&
|
|
165
|
+
this.cacheSignature === signature &&
|
|
166
|
+
this.searchableTextCache.length === rows.length
|
|
167
|
+
) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.searchableTextCache = rows.map(row => {
|
|
172
|
+
const baseText = columns
|
|
173
|
+
.map(column => this.getCellSearchValue(row, column, searchMode))
|
|
174
|
+
.join(' ');
|
|
175
|
+
|
|
176
|
+
return caseInsensitive ? baseText.toLowerCase() : baseText;
|
|
177
|
+
});
|
|
178
|
+
this.cacheSignature = signature;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private matchesFilterTree(row: Row): boolean {
|
|
182
|
+
if (!this.filterTree) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return evaluateFilterTree(this.filterTree, row, {
|
|
187
|
+
caseInsensitive: this.gridService.gridOptions.caseInsensitiveFiltering ?? false,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
hasFilter(columnKey: string | number): boolean {
|
|
192
|
+
const checkNode = (node: FilterNode): boolean => {
|
|
193
|
+
if (node.type === 'condition') {
|
|
194
|
+
return node.columnKey === columnKey;
|
|
195
|
+
}
|
|
196
|
+
if (node.type === 'group') {
|
|
197
|
+
return node.filters.some(checkNode);
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (!this.filterTree) return false;
|
|
203
|
+
return checkNode(this.filterTree);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
addFilter(filterNode: FilterNode): void {
|
|
207
|
+
const columnKeys = this.getColumnKeys(filterNode);
|
|
208
|
+
|
|
209
|
+
// Start with existing filters (if any)
|
|
210
|
+
let newFilters: FilterNode[] = [];
|
|
211
|
+
if (this.filterTree && this.filterTree.type === 'group') {
|
|
212
|
+
// Remove all filters for the affected column(s)
|
|
213
|
+
newFilters = this.filterTree.filters.filter(f => {
|
|
214
|
+
let keys: (string | number)[];
|
|
215
|
+
if (f.type === 'condition') {
|
|
216
|
+
keys = [f.columnKey];
|
|
217
|
+
} else if (f.type === 'group') {
|
|
218
|
+
keys = f.filters.reduce<(string | number)[]>((acc, ff) => {
|
|
219
|
+
if (ff.type === 'condition') {
|
|
220
|
+
acc.push(ff.columnKey);
|
|
221
|
+
}
|
|
222
|
+
return acc;
|
|
223
|
+
}, []);
|
|
224
|
+
} else {
|
|
225
|
+
keys = [];
|
|
226
|
+
}
|
|
227
|
+
return !keys.some(k => columnKeys.includes(k));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Add the new filter node
|
|
232
|
+
newFilters.push(filterNode);
|
|
233
|
+
|
|
234
|
+
// Set the new filter tree
|
|
235
|
+
this.setFilterTree({
|
|
236
|
+
type: 'group',
|
|
237
|
+
operator: 'and',
|
|
238
|
+
filters: newFilters
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
resetFilters(): void {
|
|
243
|
+
this.setFilterTree(null);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
resetFilter(columnKey: string | number): void {
|
|
247
|
+
if (!this.filterTree || this.filterTree.type !== 'group') return;
|
|
248
|
+
|
|
249
|
+
// Remove any filter node (condition or group) that applies to this column
|
|
250
|
+
const newFilters = this.filterTree.filters.filter(
|
|
251
|
+
f => !this.getColumnKeys(f).includes(columnKey)
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// If no filters left, set to null
|
|
255
|
+
if (newFilters.length === 0) {
|
|
256
|
+
this.setFilterTree(null);
|
|
257
|
+
} else {
|
|
258
|
+
this.setFilterTree({
|
|
259
|
+
...this.filterTree,
|
|
260
|
+
filters: newFilters
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
getFilterIndexes(): number[] | null {
|
|
266
|
+
return this.filteredIndexes$.getValue();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getFilters(): FilterCondition[] {
|
|
270
|
+
if (!this.filterTree || this.filterTree.type !== 'group') return [];
|
|
271
|
+
return this.filterTree.filters.filter(
|
|
272
|
+
f => f.type === 'condition'
|
|
273
|
+
) as FilterCondition[];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private updateFilterIndexes(): void {
|
|
277
|
+
const rows = this.gridService.getRows();
|
|
278
|
+
|
|
279
|
+
if (!this.gridService.shouldApplyClientSideFiltering()) {
|
|
280
|
+
this.filteredIndexes$.next(null);
|
|
281
|
+
this.gridService.setIndexMapping(
|
|
282
|
+
new Map([...Array(rows.length).keys()].map(i => [i, i]))
|
|
283
|
+
);
|
|
284
|
+
this.gridService.updateRenderedRows();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!this.filterTree && !this.globalSearch) {
|
|
289
|
+
// No filters: show all rows, identity mapping
|
|
290
|
+
this.filteredIndexes$.next(null);
|
|
291
|
+
this.gridService.setIndexMapping(
|
|
292
|
+
new Map([...Array(this.gridService.rowCount()).keys()].map(i => [i, i]))
|
|
293
|
+
);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let globalSearchQuery: string | null = null;
|
|
298
|
+
if (this.globalSearch) {
|
|
299
|
+
const columns = this.getSearchColumns(this.globalSearch.options);
|
|
300
|
+
const caseInsensitive = this.globalSearch.options?.caseInsensitive ?? this.gridService.gridOptions.caseInsensitiveFiltering ?? false;
|
|
301
|
+
const searchMode = this.globalSearch.options?.searchMode ?? 'value';
|
|
302
|
+
globalSearchQuery = caseInsensitive ? this.globalSearch.query.toLowerCase() : this.globalSearch.query;
|
|
303
|
+
|
|
304
|
+
this.ensureSearchCache(rows, columns, caseInsensitive, searchMode);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Find all matching row indexes using both filter tree and global search.
|
|
308
|
+
const matchingIndexes = rows
|
|
309
|
+
.map((row, idx) => {
|
|
310
|
+
const matchesTree = this.matchesFilterTree(row);
|
|
311
|
+
if (!matchesTree) {
|
|
312
|
+
return -1;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!globalSearchQuery) {
|
|
316
|
+
return idx;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const searchableText = this.searchableTextCache?.[idx] ?? '';
|
|
320
|
+
return searchableText.includes(globalSearchQuery) ? idx : -1;
|
|
321
|
+
})
|
|
322
|
+
.filter(idx => idx !== -1);
|
|
323
|
+
|
|
324
|
+
// Update index mapping for performance and downstream features
|
|
325
|
+
this.gridService.clearIndexMap();
|
|
326
|
+
matchingIndexes.forEach((originalIdx, filteredIdx) => {
|
|
327
|
+
this.gridService.setIndexMappingValue(filteredIdx, originalIdx);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this.filteredIndexes$.next(matchingIndexes);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
applyFilter(rows: Row[]): Row[] {
|
|
334
|
+
const filteredIndexes = this.filteredIndexes$.getValue();
|
|
335
|
+
|
|
336
|
+
if (filteredIndexes === null) {
|
|
337
|
+
return rows;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.gridService.clearIndexMap();
|
|
341
|
+
filteredIndexes.forEach((filteredIndex, i) => {
|
|
342
|
+
this.gridService.setIndexMappingValue(i, filteredIndex);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return filteredIndexes.map(index => rows[index]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { GridService } from "../GridService";
|
|
2
|
+
import { Column, DisplayValue, Row } from "../types/grid";
|
|
3
|
+
import { format } from 'date-fns';
|
|
4
|
+
import { toZonedTime } from 'date-fns-tz';
|
|
5
|
+
import { logger } from "@open-dcp/utilities";
|
|
6
|
+
|
|
7
|
+
function formatInTimeZone(
|
|
8
|
+
timestamp: number | Date,
|
|
9
|
+
timeZone: string,
|
|
10
|
+
formatString: string
|
|
11
|
+
): string {
|
|
12
|
+
const date = typeof timestamp === "number" ? new Date(timestamp) : timestamp;
|
|
13
|
+
const zonedDate = toZonedTime(date, timeZone);
|
|
14
|
+
return format(zonedDate, formatString);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class FormattingService {
|
|
18
|
+
private readonly gridService: GridService;
|
|
19
|
+
|
|
20
|
+
constructor(gridService: GridService) {
|
|
21
|
+
this.gridService = gridService;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
applyFormattingToRow(row: Row): Row {
|
|
25
|
+
const columns = this.gridService.getColumns();
|
|
26
|
+
const formattedRow: Row = { ...row }; // Clone the row to keep it immutable
|
|
27
|
+
|
|
28
|
+
if (columns && columns.length > 0) {
|
|
29
|
+
// Columns are ready, proceed with formatting
|
|
30
|
+
columns.forEach((column: Column) => {
|
|
31
|
+
const cellData = row[column.key] ?? null;
|
|
32
|
+
|
|
33
|
+
const rawValue = cellData && typeof cellData === 'object' && 'raw' in cellData
|
|
34
|
+
? cellData.raw
|
|
35
|
+
: cellData;
|
|
36
|
+
|
|
37
|
+
const value = this.formatCellValue(row, column);
|
|
38
|
+
|
|
39
|
+
// Store raw and value
|
|
40
|
+
formattedRow[column.key] = {
|
|
41
|
+
raw: rawValue, // Ensure raw value stays the same
|
|
42
|
+
value: value, // Store transformed value for sorting/filtering
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
Object.keys(row).forEach(key => {
|
|
47
|
+
formattedRow[key] = {
|
|
48
|
+
raw: row[key], // Use original value as raw
|
|
49
|
+
value: null, // No transformation yet
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
//logger.warn('Columns are not available yet. Row formatting is deferred.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return formattedRow;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private formatCustomCell(cellValue: any, row: Row, column: Column): DisplayValue {
|
|
60
|
+
let displayValue: string | undefined = undefined;
|
|
61
|
+
let renderedElement: HTMLElement | undefined = undefined;
|
|
62
|
+
if (typeof column.renderCell === 'function') {
|
|
63
|
+
try {
|
|
64
|
+
const functionReturn: string | HTMLElement = column.renderCell(cellValue, row);
|
|
65
|
+
if (functionReturn instanceof HTMLAnchorElement) {
|
|
66
|
+
renderedElement = functionReturn;
|
|
67
|
+
} else if (typeof functionReturn == "string") {
|
|
68
|
+
displayValue = functionReturn;
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error(`Error in custom renderCell function for column ${column.key}:`, error);
|
|
72
|
+
displayValue = 'Error';
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
logger.warn(`Custom column type requires a renderCell function.`);
|
|
76
|
+
displayValue = 'N/A';
|
|
77
|
+
}
|
|
78
|
+
return { display: displayValue, renderedElement };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private formatDateCell(cellValue: any, column: Column): string {
|
|
82
|
+
const date = new Date(cellValue);
|
|
83
|
+
let dateFormatString = this.gridService.displayFormatSubject.getValue().dateFormat;
|
|
84
|
+
if (column.type === 'datetime') {
|
|
85
|
+
dateFormatString += ` ${this.gridService.displayFormatSubject.getValue().timeFormat}`;
|
|
86
|
+
}
|
|
87
|
+
return formatInTimeZone(
|
|
88
|
+
date,
|
|
89
|
+
this.gridService.displayFormatSubject.getValue().timeZone,
|
|
90
|
+
dateFormatString
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
formatCellDisplay(row: Row, column: Column): DisplayValue {
|
|
95
|
+
const cellValue: any = row[column.key]?.value ?? null;
|
|
96
|
+
if (cellValue === null) {
|
|
97
|
+
return { display: null, renderedElement: undefined };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let displayValue: string | undefined = undefined;
|
|
101
|
+
let renderedElement: HTMLElement | undefined = undefined;
|
|
102
|
+
|
|
103
|
+
if (typeof column.transformer === 'function') {
|
|
104
|
+
try {
|
|
105
|
+
const transformed = column.transformer(cellValue);
|
|
106
|
+
displayValue = transformed != null ? transformed.toString() : '';
|
|
107
|
+
return { display: displayValue, renderedElement };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error(`Error in transformer for column ${column.key}:`, error);
|
|
110
|
+
return { display: 'Error', renderedElement };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
switch (column.type) {
|
|
116
|
+
case 'component':
|
|
117
|
+
break;
|
|
118
|
+
case 'custom':
|
|
119
|
+
return this.formatCustomCell(cellValue, row, column);
|
|
120
|
+
case 'integer':
|
|
121
|
+
case 'number':
|
|
122
|
+
displayValue = cellValue.toString();
|
|
123
|
+
break;
|
|
124
|
+
case 'date':
|
|
125
|
+
case 'datetime':
|
|
126
|
+
displayValue = this.formatDateCell(cellValue, column);
|
|
127
|
+
break;
|
|
128
|
+
case 'duration':
|
|
129
|
+
displayValue = `${Math.floor(cellValue / 3600).toString().padStart(2, '0')}:${Math.floor((cellValue % 3600) / 60).toString().padStart(2, '0')}:${(cellValue % 60).toString().padStart(2, '0')}`;
|
|
130
|
+
break;
|
|
131
|
+
case 'string':
|
|
132
|
+
default:
|
|
133
|
+
displayValue = cellValue.toString();
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.error(`Error formatting cell for column ${column.key}:`, error);
|
|
138
|
+
displayValue = 'Error';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { display: displayValue, renderedElement };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private transformCustomOrComponent(rawValue: any, column: Column): any {
|
|
145
|
+
if (typeof column.transformer === 'function') {
|
|
146
|
+
try {
|
|
147
|
+
return column.transformer(rawValue);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.error(`Error in custom transformer function for column ${column.key}:`, error);
|
|
150
|
+
return 'Error';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return rawValue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private transformInteger(rawValue: any): number | null {
|
|
157
|
+
return rawValue !== null ? parseInt(rawValue, 10) : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private transformNumber(rawValue: any): number | null {
|
|
161
|
+
return rawValue !== null ? parseFloat(rawValue) : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private transformDate(rawValue: any): number | null {
|
|
165
|
+
if (rawValue !== null) {
|
|
166
|
+
const date = new Date(rawValue);
|
|
167
|
+
return isNaN(date.getTime()) ? null : date.getTime();
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private transformDuration(rawValue: any): number | null {
|
|
173
|
+
return rawValue !== null ? parseInt(rawValue, 10) : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private transformString(rawValue: any): string | null {
|
|
177
|
+
return rawValue !== null ? rawValue.toString() : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
formatCellValue(row: Row, column: Column): any {
|
|
181
|
+
// Safely retrieve the raw value
|
|
182
|
+
const cellData = row[column.key] ?? null;
|
|
183
|
+
const rawValue: any = cellData && typeof cellData === 'object' && 'raw' in cellData
|
|
184
|
+
? cellData.raw
|
|
185
|
+
: cellData;
|
|
186
|
+
|
|
187
|
+
switch (column.type) {
|
|
188
|
+
case 'component':
|
|
189
|
+
case 'custom':
|
|
190
|
+
return this.transformCustomOrComponent(rawValue, column);
|
|
191
|
+
case 'integer':
|
|
192
|
+
return this.transformInteger(rawValue);
|
|
193
|
+
case 'number':
|
|
194
|
+
return this.transformNumber(rawValue);
|
|
195
|
+
case 'date':
|
|
196
|
+
case 'datetime':
|
|
197
|
+
return this.transformDate(rawValue);
|
|
198
|
+
case 'duration':
|
|
199
|
+
return this.transformDuration(rawValue);
|
|
200
|
+
case 'string':
|
|
201
|
+
default:
|
|
202
|
+
return this.transformString(rawValue);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { GridService } from "../GridService";
|
|
2
|
+
|
|
3
|
+
export class RowHeightService {
|
|
4
|
+
private rowHeights: Map<number, number> = new Map();
|
|
5
|
+
private heightCache: number[] = [];
|
|
6
|
+
private readonly gridService: GridService;
|
|
7
|
+
|
|
8
|
+
constructor(gridService: GridService) {
|
|
9
|
+
this.gridService = gridService;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
initRowHeights(numberOfRows: number) {
|
|
13
|
+
this.rowHeights = new Map(
|
|
14
|
+
Array.from({ length: numberOfRows }, (_, index) => [index, this.gridService.elementHeight])
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getRowHeight(index: number): number {
|
|
19
|
+
return this.rowHeights.get(index) ?? this.gridService.elementHeight
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getHeightCache(): number[] {
|
|
23
|
+
return this.heightCache;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
updateRowHeight(index: number, height: number) {
|
|
27
|
+
this.rowHeights.set(index, height);
|
|
28
|
+
this.recalculateHeights();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
recalculateHeights() {
|
|
32
|
+
let filteredRows = this.gridService.getFilterIndexes();
|
|
33
|
+
|
|
34
|
+
this.heightCache = [];
|
|
35
|
+
let sum = 0;
|
|
36
|
+
for (let i = 0; i < this.gridService.rowCount(); i++) {
|
|
37
|
+
let rowIndex = filteredRows ? filteredRows[i] : i;
|
|
38
|
+
sum += this.getRowHeight(rowIndex);
|
|
39
|
+
this.heightCache.push(sum);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
updateRowHeights() {
|
|
44
|
+
let container = this.gridService.getContainer();
|
|
45
|
+
if (!container) return;
|
|
46
|
+
|
|
47
|
+
const rows = container.querySelectorAll('tbody > tr:not(.firstRow):not(.lastRow)'); // Adjust the selector as needed
|
|
48
|
+
|
|
49
|
+
let hasUpdated = false;
|
|
50
|
+
|
|
51
|
+
rows.forEach((row, index) => {
|
|
52
|
+
const rowHeight = row.getBoundingClientRect().height; // Get the actual row height
|
|
53
|
+
const rowIndex = index + this.gridService.startIndex;
|
|
54
|
+
|
|
55
|
+
if (this.getRowHeight(rowIndex) !== rowHeight) {
|
|
56
|
+
this.updateRowHeight(rowIndex, rowHeight);
|
|
57
|
+
hasUpdated = true;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (hasUpdated) {
|
|
62
|
+
this.gridService.updateRenderedRows();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
findStartIndex(scrollTop: number): number {
|
|
67
|
+
let index = 0;
|
|
68
|
+
while (index < this.heightCache.length && this.heightCache[index] < scrollTop) {
|
|
69
|
+
index++;
|
|
70
|
+
}
|
|
71
|
+
return Math.max(0, index);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
findEndIndex(scrollBottom: number): number {
|
|
75
|
+
let index = 0;
|
|
76
|
+
while (index < this.heightCache.length && this.heightCache[index] < scrollBottom) {
|
|
77
|
+
index++;
|
|
78
|
+
}
|
|
79
|
+
return Math.min(index, this.heightCache.length - 1)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { BehaviorSubject } from 'rxjs';
|
|
2
|
+
|
|
3
|
+
export class SelectionService {
|
|
4
|
+
private readonly selectedIndexes$: BehaviorSubject<Set<number>>;
|
|
5
|
+
private readonly multiSelect: boolean;
|
|
6
|
+
|
|
7
|
+
constructor(selections: Set<number> = new Set(), multiSelect: boolean = true) {
|
|
8
|
+
this.multiSelect = multiSelect;
|
|
9
|
+
this.selectedIndexes$ = new BehaviorSubject<Set<number>>(selections);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get selected$() {
|
|
13
|
+
return this.selectedIndexes$.asObservable();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
selectRow(index: number) {
|
|
17
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
18
|
+
selected.add(index);
|
|
19
|
+
this.selectedIndexes$.next(selected);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
deselectRow(index: number) {
|
|
23
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
24
|
+
selected.delete(index);
|
|
25
|
+
this.selectedIndexes$.next(selected);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toggleRowSelection(index: number) {
|
|
29
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
30
|
+
if (selected.has(index)) {
|
|
31
|
+
selected.delete(index);
|
|
32
|
+
} else {
|
|
33
|
+
if (!this.multiSelect) {
|
|
34
|
+
selected.clear();
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
selected.add(index);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.selectedIndexes$.next(selected);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clearSelection() {
|
|
44
|
+
this.selectedIndexes$.next(new Set());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
selectAll(indices: number[]) {
|
|
48
|
+
this.selectedIndexes$.next(new Set(indices));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
deselectAll() {
|
|
52
|
+
this.clearSelection();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
shiftSelect(startIndex: number, endIndex: number) {
|
|
56
|
+
if (!this.multiSelect) return;
|
|
57
|
+
|
|
58
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
59
|
+
const [start, end] = [Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)];
|
|
60
|
+
|
|
61
|
+
for (let i = start; i <= end; i++) {
|
|
62
|
+
selected.add(i);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.selectedIndexes$.next(selected);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isSelected(index: number): boolean {
|
|
69
|
+
return this.selectedIndexes$.value.has(index);
|
|
70
|
+
}
|
|
71
|
+
}
|