@opencorestack/opengridx 0.1.8 → 1.0.0
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/CHANGELOG.md +59 -0
- package/dist/index.d.ts +60 -41
- package/dist/opengridx.es.js +5271 -4660
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/README.md +14 -0
- package/docs/components/aggregation-footer.md +48 -0
- package/docs/components/datagrid.md +3 -0
- package/docs/components/empty-state.md +50 -0
- package/docs/components/error-overlay.md +40 -0
- package/docs/roadmap.md +20 -1
- package/lib/components/Cell/Cell.tsx +61 -64
- package/lib/components/Cell/GridEditInputCell.tsx +4 -4
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +132 -0
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +58 -0
- package/lib/components/DataGrid/DataGrid.tsx +396 -1654
- package/lib/components/DataGrid/GridAggregationFooter.tsx +68 -0
- package/lib/components/DataGrid/GridEmptyState.tsx +16 -0
- package/lib/components/DataGrid/GridErrorOverlay.tsx +33 -0
- package/lib/components/DataGrid/GridListView.tsx +116 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +98 -0
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +124 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +202 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +18 -10
- package/lib/components/Header/ColumnMenu.tsx +2 -2
- package/lib/components/Header/Header.tsx +4 -4
- package/lib/components/ListView/ListViewRow.tsx +2 -2
- package/lib/components/Row/Row.tsx +46 -45
- package/lib/components/SkeletonRow/SkeletonRow.tsx +9 -2
- package/lib/components/Toolbar/GlobalSearch.tsx +1 -1
- package/lib/components/Toolbar/GridToolbar.tsx +26 -5
- package/lib/components/Toolbar/PivotPanel.tsx +7 -2
- package/lib/components/ui/DragHandleIcon.tsx +1 -1
- package/lib/hooks/core/useDataGrid.regression.test.ts +66 -0
- package/lib/hooks/core/useDataGrid.ts +15 -35
- package/lib/hooks/core/useGridColumns.tsx +228 -0
- package/lib/hooks/core/useGridControlledState.ts +192 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +371 -0
- package/lib/hooks/core/useGridRowPipeline.ts +109 -0
- package/lib/hooks/core/useGridScrollSync.ts +52 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +63 -0
- package/lib/hooks/core/useGridVirtualization.ts +186 -0
- package/lib/hooks/core/useGridVisibleRows.ts +66 -0
- package/lib/hooks/core/useLayout.ts +348 -0
- package/lib/hooks/features/useAggregation.test.ts +135 -0
- package/lib/hooks/features/useAggregation.ts +7 -7
- package/lib/hooks/features/useGridClipboard.ts +1 -1
- package/lib/hooks/features/useGridDataSource.ts +2 -2
- package/lib/hooks/features/useGridEditing.ts +31 -32
- package/lib/hooks/features/usePivot.test.ts +202 -0
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/useColumnReorder.ts +1 -1
- package/lib/hooks/useRowGrouping.ts +14 -14
- package/lib/hooks/useTreeData.ts +1 -1
- package/lib/state/types.ts +1 -1
- package/lib/state/useGridStateStorage.ts +2 -2
- package/lib/types/index.ts +48 -32
- package/lib/utils/export/exportToExcelAdvanced.ts +14 -14
- package/lib/utils/export/index.ts +23 -18
- package/lib/utils/filtering/filtering.test.ts +178 -0
- package/lib/utils/filtering/index.ts +20 -20
- package/lib/utils/sorting/index.ts +1 -1
- package/lib/utils/sorting/sorting.test.ts +88 -0
- package/package.json +12 -5
- package/lib/utils/virtualization/index.ts +0 -106
package/lib/types/index.ts
CHANGED
|
@@ -7,8 +7,24 @@ export type GridRowId = string | number;
|
|
|
7
7
|
export interface GridRowModel {
|
|
8
8
|
/** Uniquely identifies the row. */
|
|
9
9
|
id: GridRowId;
|
|
10
|
+
/** Internal: row has child nodes (tree data / row grouping). */
|
|
11
|
+
_hasChildren?: boolean;
|
|
12
|
+
/** Internal: row is a skeleton placeholder during infinite-scroll loading. */
|
|
13
|
+
_isSkeleton?: boolean;
|
|
14
|
+
/** Internal: depth in the tree or grouping hierarchy. */
|
|
15
|
+
_treeDepth?: number;
|
|
16
|
+
/** Internal: whether the group/tree node is currently expanded. */
|
|
17
|
+
_isExpanded?: boolean;
|
|
18
|
+
/** Internal: field used for grouping at this level. */
|
|
19
|
+
_groupingField?: string;
|
|
20
|
+
/** Internal: grouping field value at this level. */
|
|
21
|
+
_groupingValue?: unknown;
|
|
22
|
+
/** Internal: total descendant count for tree/grouping nodes. */
|
|
23
|
+
_descendantCount?: number;
|
|
24
|
+
/** Internal: server-reported child count for lazy-loaded tree nodes. */
|
|
25
|
+
serverChildrenCount?: number;
|
|
10
26
|
/** Any other dynamic row properties. */
|
|
11
|
-
[key: string]:
|
|
27
|
+
[key: string]: unknown;
|
|
12
28
|
}
|
|
13
29
|
|
|
14
30
|
export type GridAlignment = 'left' | 'center' | 'right';
|
|
@@ -27,10 +43,10 @@ export interface GridTreeNode {
|
|
|
27
43
|
/** The field this node is grouping by. */
|
|
28
44
|
groupingField?: string;
|
|
29
45
|
/** The actual value being grouped. */
|
|
30
|
-
groupingValue?:
|
|
46
|
+
groupingValue?: unknown;
|
|
31
47
|
|
|
32
48
|
/** Computed aggregation results for this group. */
|
|
33
|
-
aggregatedValues?: Record<string,
|
|
49
|
+
aggregatedValues?: Record<string, unknown>;
|
|
34
50
|
/** Position of the aggregation row relative to the group. */
|
|
35
51
|
aggregationPosition?: 'inline' | 'footer' | null;
|
|
36
52
|
/** Current expansion state. */
|
|
@@ -91,13 +107,15 @@ export interface GridColDef<R extends GridRowModel = GridRowModel> {
|
|
|
91
107
|
hideable?: boolean;
|
|
92
108
|
/** If false, the column cannot be pinned. */
|
|
93
109
|
pinnable?: boolean;
|
|
110
|
+
/** Internal: marks this column as a virtual spacer used for horizontal virtualization. */
|
|
111
|
+
isSpacer?: boolean;
|
|
94
112
|
/** Type used to determine default operators and formatting logic. */
|
|
95
113
|
type?: 'string' | 'number' | 'date' | 'boolean' | 'singleSelect' | 'image';
|
|
96
114
|
/** Options used for 'singleSelect' type. */
|
|
97
|
-
valueOptions?: Array<string | number | { value:
|
|
115
|
+
valueOptions?: Array<string | number | { value: unknown; label: string }>;
|
|
98
116
|
|
|
99
117
|
/** Function to compute a cell value from raw row data. */
|
|
100
|
-
valueGetter?: (params: GridValueGetterParams<R>) =>
|
|
118
|
+
valueGetter?: (params: GridValueGetterParams<R>) => unknown;
|
|
101
119
|
/** Function to format a value into a human-readable string. */
|
|
102
120
|
valueFormatter?: (params: GridValueFormatterParams<R>) => string;
|
|
103
121
|
/** Custom component or element to render in the cell. */
|
|
@@ -186,13 +204,13 @@ export interface GridValueGetterParams<R extends GridRowModel = GridRowModel> {
|
|
|
186
204
|
/** The field name. */
|
|
187
205
|
field: string;
|
|
188
206
|
/** The raw value from the row object. */
|
|
189
|
-
value:
|
|
207
|
+
value: unknown;
|
|
190
208
|
}
|
|
191
209
|
|
|
192
210
|
/** Parameters passed to the `valueFormatter` function. */
|
|
193
211
|
export interface GridValueFormatterParams<R extends GridRowModel = GridRowModel> {
|
|
194
212
|
/** The raw value to format. */
|
|
195
|
-
value:
|
|
213
|
+
value: unknown;
|
|
196
214
|
/** The row object. */
|
|
197
215
|
row: R;
|
|
198
216
|
/** The field name. */
|
|
@@ -202,7 +220,7 @@ export interface GridValueFormatterParams<R extends GridRowModel = GridRowModel>
|
|
|
202
220
|
/** Parameters passed to the `renderCell` function. */
|
|
203
221
|
export interface GridRenderCellParams<R extends GridRowModel = GridRowModel> {
|
|
204
222
|
/** The current cell value. */
|
|
205
|
-
value:
|
|
223
|
+
value: unknown;
|
|
206
224
|
/** The row object. */
|
|
207
225
|
row: R;
|
|
208
226
|
/** The field name and unique ID. */
|
|
@@ -256,7 +274,7 @@ export interface GridFilterItem {
|
|
|
256
274
|
/** The comparison operator. */
|
|
257
275
|
operator: GridFilterOperator;
|
|
258
276
|
/** The value to compare against. */
|
|
259
|
-
value?:
|
|
277
|
+
value?: unknown;
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
/** Describes a group of filters combined with a logical operator. */
|
|
@@ -325,7 +343,7 @@ export interface GridGetRowsParams {
|
|
|
325
343
|
export interface GridGetRowsResponse<R extends GridRowModel = GridRowModel> {
|
|
326
344
|
rows: R[];
|
|
327
345
|
rowCount?: number;
|
|
328
|
-
aggregationResults?: Record<string,
|
|
346
|
+
aggregationResults?: Record<string, unknown>;
|
|
329
347
|
}
|
|
330
348
|
|
|
331
349
|
/**
|
|
@@ -341,7 +359,7 @@ export interface GridDataSource<R extends GridRowModel = GridRowModel> {
|
|
|
341
359
|
/**
|
|
342
360
|
* Optional: Fetches aggregation results for the entire dataset.
|
|
343
361
|
*/
|
|
344
|
-
getAggregations?: (params: Omit<GridGetRowsParams, 'startRow' | 'endRow'>) => Promise<Record<string,
|
|
362
|
+
getAggregations?: (params: Omit<GridGetRowsParams, 'startRow' | 'endRow'>) => Promise<Record<string, unknown>>;
|
|
345
363
|
}
|
|
346
364
|
|
|
347
365
|
export interface GridPaginationModel {
|
|
@@ -363,19 +381,17 @@ export interface GridPinnedRows<R extends GridRowModel = GridRowModel> {
|
|
|
363
381
|
|
|
364
382
|
export interface GridVirtualizationState {
|
|
365
383
|
renderContext: GridRenderContext;
|
|
366
|
-
scrollTop: number;
|
|
367
|
-
scrollLeft: number;
|
|
368
384
|
}
|
|
369
385
|
|
|
370
386
|
export type GridRowGroupingModel = string[];
|
|
371
387
|
|
|
372
|
-
export type GridAggregationFunction = (values:
|
|
388
|
+
export type GridAggregationFunction = (values: unknown[]) => unknown;
|
|
373
389
|
|
|
374
390
|
export interface GridAggregationModel {
|
|
375
|
-
[field: string]: string;
|
|
391
|
+
[field: string]: string;
|
|
376
392
|
}
|
|
377
393
|
|
|
378
|
-
export type GridAggregationResult = Record<string,
|
|
394
|
+
export type GridAggregationResult = Record<string, unknown>;
|
|
379
395
|
|
|
380
396
|
export type GridAggregationPosition = 'footer' | 'inline' | 'both';
|
|
381
397
|
|
|
@@ -400,7 +416,7 @@ export interface GridRenderContext {
|
|
|
400
416
|
lastColumnIndex: number;
|
|
401
417
|
}
|
|
402
418
|
|
|
403
|
-
export interface
|
|
419
|
+
export interface GridInternalState {
|
|
404
420
|
rows: {
|
|
405
421
|
idRowsLookup: Map<GridRowId, GridRowModel>;
|
|
406
422
|
allRows: GridRowId[];
|
|
@@ -437,7 +453,7 @@ export interface GridState {
|
|
|
437
453
|
};
|
|
438
454
|
dataSource: {
|
|
439
455
|
loading: boolean;
|
|
440
|
-
error:
|
|
456
|
+
error: unknown;
|
|
441
457
|
};
|
|
442
458
|
}
|
|
443
459
|
|
|
@@ -603,7 +619,7 @@ export interface DataGridProps<R extends GridRowModel = GridRowModel> {
|
|
|
603
619
|
/** Callback to process an updated row after inline editing. Supports promises and validation. */
|
|
604
620
|
processRowUpdate?: (newRow: R, oldRow: R) => R | Promise<R>;
|
|
605
621
|
/** Callback fired if `processRowUpdate` throws or rejects. */
|
|
606
|
-
onProcessRowUpdateError?: (error:
|
|
622
|
+
onProcessRowUpdateError?: (error: unknown) => void;
|
|
607
623
|
|
|
608
624
|
/** Interface for connecting the grid to an external (server-side) data source. */
|
|
609
625
|
dataSource?: GridDataSource<R>;
|
|
@@ -616,23 +632,23 @@ export interface DataGridProps<R extends GridRowModel = GridRowModel> {
|
|
|
616
632
|
/** Custom components to replace internal grid parts. */
|
|
617
633
|
slots?: {
|
|
618
634
|
/** Component rendered as the grid toolbar. */
|
|
619
|
-
toolbar?: React.ComponentType<
|
|
635
|
+
toolbar?: React.ComponentType<Record<string, unknown>>;
|
|
620
636
|
/** Component rendered as the pagination control. */
|
|
621
|
-
pagination?: React.ComponentType<
|
|
637
|
+
pagination?: React.ComponentType<Record<string, unknown>>;
|
|
622
638
|
/** Component rendered when the grid is empty. */
|
|
623
|
-
noRowsOverlay?: React.ComponentType<
|
|
639
|
+
noRowsOverlay?: React.ComponentType<Record<string, unknown>>;
|
|
624
640
|
/** Component rendered during loading states. */
|
|
625
|
-
loadingOverlay?: React.ComponentType<
|
|
641
|
+
loadingOverlay?: React.ComponentType<Record<string, unknown>>;
|
|
626
642
|
/** Component rendered at the very bottom of the grid. */
|
|
627
|
-
footer?: React.ComponentType<
|
|
643
|
+
footer?: React.ComponentType<Record<string, unknown>>;
|
|
628
644
|
};
|
|
629
645
|
/** Properties passed directly to custom slots. */
|
|
630
646
|
slotProps?: {
|
|
631
|
-
toolbar?:
|
|
632
|
-
pagination?:
|
|
633
|
-
noRowsOverlay?:
|
|
634
|
-
loadingOverlay?:
|
|
635
|
-
footer?:
|
|
647
|
+
toolbar?: Record<string, unknown>;
|
|
648
|
+
pagination?: Record<string, unknown>;
|
|
649
|
+
noRowsOverlay?: Record<string, unknown>;
|
|
650
|
+
loadingOverlay?: Record<string, unknown>;
|
|
651
|
+
footer?: Record<string, unknown>;
|
|
636
652
|
};
|
|
637
653
|
|
|
638
654
|
/** When true, renders the grid as a single-column list of cards. Perfect for mobile/responsive views. */
|
|
@@ -647,7 +663,7 @@ export interface DataGridProps<R extends GridRowModel = GridRowModel> {
|
|
|
647
663
|
columnGroupingModel?: GridColumnGroupingModel;
|
|
648
664
|
}
|
|
649
665
|
|
|
650
|
-
export interface GridEditCellProps<V =
|
|
666
|
+
export interface GridEditCellProps<V = unknown> {
|
|
651
667
|
id: GridRowId;
|
|
652
668
|
field: string;
|
|
653
669
|
value?: V;
|
|
@@ -669,7 +685,7 @@ export interface GridRowParams<R extends GridRowModel = GridRowModel> {
|
|
|
669
685
|
export interface GridCellParams<R extends GridRowModel = GridRowModel> {
|
|
670
686
|
row: R;
|
|
671
687
|
field: string;
|
|
672
|
-
value:
|
|
688
|
+
value: unknown;
|
|
673
689
|
colDef: GridColDef<R>;
|
|
674
690
|
rowIndex: number;
|
|
675
691
|
colIndex: number;
|
|
@@ -707,7 +723,7 @@ export interface GridApi {
|
|
|
707
723
|
/** Returns all rows currently visible after filtering and sorting. */
|
|
708
724
|
getVisibleRows: () => GridRowModel[];
|
|
709
725
|
/** Returns the current aggregation results. */
|
|
710
|
-
getAggregationResult: () => Record<string,
|
|
726
|
+
getAggregationResult: () => Record<string, unknown> | null;
|
|
711
727
|
/** Returns the active aggregation configuration. */
|
|
712
728
|
getAggregationModel: () => GridAggregationModel | null;
|
|
713
729
|
|
|
@@ -63,7 +63,7 @@ export interface ExcelAdvancedExportOptions {
|
|
|
63
63
|
/** Body font size (default: 10) */
|
|
64
64
|
bodyFontSize?: number;
|
|
65
65
|
/** Current aggregation result */
|
|
66
|
-
aggregationResult?: Record<string,
|
|
66
|
+
aggregationResult?: Record<string, unknown> | null;
|
|
67
67
|
/** Current aggregation model */
|
|
68
68
|
aggregationModel?: GridAggregationModel | null;
|
|
69
69
|
/** Currently selected row IDs */
|
|
@@ -72,7 +72,7 @@ export interface ExcelAdvancedExportOptions {
|
|
|
72
72
|
|
|
73
73
|
// ─── Cell type inference ──────────────────────────────────────────────────────
|
|
74
74
|
|
|
75
|
-
function inferCellType(colDef: GridColDef<
|
|
75
|
+
function inferCellType(colDef: GridColDef<GridRowModel>, value: unknown): 'number' | 'boolean' | 'date' | 'string' {
|
|
76
76
|
if (colDef.type === 'number') return 'number';
|
|
77
77
|
if (colDef.type === 'boolean') return 'boolean';
|
|
78
78
|
if (colDef.type === 'date') return 'date';
|
|
@@ -84,7 +84,7 @@ function inferCellType(colDef: GridColDef<any>, value: any): 'number' | 'boolean
|
|
|
84
84
|
|
|
85
85
|
// ─── Default numFmt per column type ──────────────────────────────────────────
|
|
86
86
|
|
|
87
|
-
function defaultNumFmt(colDef: GridColDef<
|
|
87
|
+
function defaultNumFmt(colDef: GridColDef<GridRowModel>): string | undefined {
|
|
88
88
|
if (colDef.type === 'number') return '#,##0.##';
|
|
89
89
|
if (colDef.type === 'date') return 'yyyy-mm-dd';
|
|
90
90
|
return undefined;
|
|
@@ -92,7 +92,7 @@ function defaultNumFmt(colDef: GridColDef<any>): string | undefined {
|
|
|
92
92
|
|
|
93
93
|
// ─── Column display width (chars) ────────────────────────────────────────────
|
|
94
94
|
|
|
95
|
-
function colWidth(colDef: GridColDef<
|
|
95
|
+
function colWidth(colDef: GridColDef<GridRowModel>, override?: number): number {
|
|
96
96
|
if (override != null) return override;
|
|
97
97
|
const px = (colDef.width ?? 120) as number;
|
|
98
98
|
return Math.min(60, Math.max(8, (px - 5) / 7));
|
|
@@ -195,18 +195,18 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
195
195
|
workbook.creator = 'OpenGridX';
|
|
196
196
|
workbook.created = new Date();
|
|
197
197
|
|
|
198
|
-
// Filter system columns
|
|
199
|
-
const exportColumns: GridColDef<
|
|
198
|
+
// Filter system columns
|
|
199
|
+
const exportColumns: GridColDef<GridRowModel>[] = (columns as unknown as GridColDef<GridRowModel>[]).filter(col =>
|
|
200
200
|
col.exportable !== false &&
|
|
201
201
|
col.field !== '__check__' &&
|
|
202
202
|
col.field !== '__actions__' &&
|
|
203
|
-
!
|
|
203
|
+
!col.isSpacer
|
|
204
204
|
);
|
|
205
205
|
|
|
206
|
-
const allRows:
|
|
207
|
-
const sRows:
|
|
206
|
+
const allRows: GridRowModel[] = rows as GridRowModel[];
|
|
207
|
+
const sRows: GridRowModel[] = (selectedRows && selectedRows.length > 0
|
|
208
208
|
? rows.filter(r => selectedRows.includes(r.id))
|
|
209
|
-
: rows) as
|
|
209
|
+
: rows) as GridRowModel[];
|
|
210
210
|
|
|
211
211
|
// Normalise sheet definitions
|
|
212
212
|
const resolvedSheets: (ExcelSheetDefinition | { type: 'summary'; name?: string })[] =
|
|
@@ -279,7 +279,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
279
279
|
const seen = new Set<string>();
|
|
280
280
|
rowsToExport.forEach(row => {
|
|
281
281
|
imageColumns.forEach(col => {
|
|
282
|
-
const val =
|
|
282
|
+
const val = row[col.field];
|
|
283
283
|
if (typeof val === 'string' && val && !imageCache.has(val) && !seen.has(val)) {
|
|
284
284
|
urlsNeeded.push(val);
|
|
285
285
|
seen.add(val);
|
|
@@ -348,7 +348,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
348
348
|
// Image columns: write empty string; image placed below
|
|
349
349
|
if (columnStyles[col.field]?.embedImage) return '';
|
|
350
350
|
|
|
351
|
-
let value:
|
|
351
|
+
let value: unknown = row[col.field];
|
|
352
352
|
if (col.valueGetter) {
|
|
353
353
|
value = col.valueGetter({ row, field: col.field, value });
|
|
354
354
|
}
|
|
@@ -399,7 +399,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
399
399
|
|
|
400
400
|
imageColumns.forEach(col => {
|
|
401
401
|
const colIdx = exportColumns.indexOf(col);
|
|
402
|
-
const url =
|
|
402
|
+
const url = row[col.field];
|
|
403
403
|
const imgId = typeof url === 'string' ? imageCache.get(url) : undefined;
|
|
404
404
|
|
|
405
405
|
if (imgId === undefined) {
|
|
@@ -433,7 +433,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
433
433
|
},
|
|
434
434
|
ext: { width: imgW, height: imgH },
|
|
435
435
|
editAs: 'oneCell',
|
|
436
|
-
} as
|
|
436
|
+
} as unknown as import('exceljs').ImageRange);
|
|
437
437
|
});
|
|
438
438
|
} else {
|
|
439
439
|
dataRow.height = 16;
|
|
@@ -7,7 +7,7 @@ export interface CsvExportOptions {
|
|
|
7
7
|
includeHeaders?: boolean;
|
|
8
8
|
delimiter?: string;
|
|
9
9
|
selectedRows?: (string | number)[];
|
|
10
|
-
aggregationResult?: Record<string,
|
|
10
|
+
aggregationResult?: Record<string, unknown> | null;
|
|
11
11
|
aggregationModel?: GridAggregationModel | null;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -48,7 +48,7 @@ export function exportToCsv<R extends GridRowModel>(
|
|
|
48
48
|
|
|
49
49
|
rowsToExport.forEach(row => {
|
|
50
50
|
const values = exportColumns.map(col => {
|
|
51
|
-
let value:
|
|
51
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
52
52
|
|
|
53
53
|
if (col.valueGetter) {
|
|
54
54
|
value = col.valueGetter({ row, field: col.field, value });
|
|
@@ -109,7 +109,7 @@ export interface ExcelExportOptions {
|
|
|
109
109
|
sheetName?: string;
|
|
110
110
|
includeHeaders?: boolean;
|
|
111
111
|
selectedRows?: (string | number)[];
|
|
112
|
-
aggregationResult?: Record<string,
|
|
112
|
+
aggregationResult?: Record<string, unknown> | null;
|
|
113
113
|
aggregationModel?: GridAggregationModel | null;
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -119,7 +119,7 @@ export function exportToExcel<R extends GridRowModel>(
|
|
|
119
119
|
options: ExcelExportOptions = {}
|
|
120
120
|
): void {
|
|
121
121
|
const {
|
|
122
|
-
fileName = 'export.
|
|
122
|
+
fileName = 'export.xls',
|
|
123
123
|
sheetName = 'Sheet1',
|
|
124
124
|
includeHeaders = true,
|
|
125
125
|
selectedRows
|
|
@@ -159,7 +159,7 @@ export function exportToExcel<R extends GridRowModel>(
|
|
|
159
159
|
rowsToExport.forEach(row => {
|
|
160
160
|
html += '<tr>';
|
|
161
161
|
exportColumns.forEach(col => {
|
|
162
|
-
let value:
|
|
162
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
163
163
|
|
|
164
164
|
if (col.valueGetter) {
|
|
165
165
|
value = col.valueGetter({ row, field: col.field, value });
|
|
@@ -227,7 +227,7 @@ export interface JsonExportOptions {
|
|
|
227
227
|
fileName?: string;
|
|
228
228
|
pretty?: boolean;
|
|
229
229
|
selectedRows?: (string | number)[];
|
|
230
|
-
aggregationResult?: Record<string,
|
|
230
|
+
aggregationResult?: Record<string, unknown> | null;
|
|
231
231
|
aggregationModel?: GridAggregationModel | null;
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -264,9 +264,9 @@ export function exportToJson<R extends GridRowModel>(
|
|
|
264
264
|
|
|
265
265
|
// Build JSON data
|
|
266
266
|
const data = rowsToExport.map(row => {
|
|
267
|
-
const obj:
|
|
267
|
+
const obj: Record<string, unknown> = {};
|
|
268
268
|
exportColumns.forEach(col => {
|
|
269
|
-
let value:
|
|
269
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
270
270
|
|
|
271
271
|
// Use valueGetter if available
|
|
272
272
|
if (col.valueGetter) {
|
|
@@ -278,23 +278,27 @@ export function exportToJson<R extends GridRowModel>(
|
|
|
278
278
|
return obj;
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
type ExportJsonPayload =
|
|
282
|
+
| Record<string, unknown>[]
|
|
283
|
+
| { data: Record<string, unknown>[]; aggregation: { labels: Record<string, unknown>; values: Record<string, unknown> } };
|
|
284
|
+
|
|
285
|
+
let outData: ExportJsonPayload = data;
|
|
282
286
|
|
|
283
287
|
if (options.aggregationResult) {
|
|
284
288
|
const aggResult = options.aggregationResult;
|
|
285
289
|
const aggModel = options.aggregationModel || {};
|
|
286
|
-
|
|
287
|
-
const summaryLabelObj:
|
|
290
|
+
|
|
291
|
+
const summaryLabelObj: Record<string, unknown> = {};
|
|
288
292
|
exportColumns.forEach((col) => {
|
|
289
293
|
if (aggModel[col.field]) {
|
|
290
294
|
summaryLabelObj[col.field] = aggModel[col.field].toUpperCase();
|
|
291
295
|
}
|
|
292
296
|
});
|
|
293
297
|
|
|
294
|
-
const summaryDataObj:
|
|
298
|
+
const summaryDataObj: Record<string, unknown> = {};
|
|
295
299
|
exportColumns.forEach((col) => {
|
|
296
300
|
if (aggResult[col.field] !== undefined && aggResult[col.field] !== null) {
|
|
297
|
-
let aggVal = aggResult[col.field];
|
|
301
|
+
let aggVal: unknown = aggResult[col.field];
|
|
298
302
|
if (col.valueFormatter) {
|
|
299
303
|
aggVal = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
300
304
|
} else if (aggModel[col.field]) {
|
|
@@ -326,7 +330,7 @@ export function exportToJson<R extends GridRowModel>(
|
|
|
326
330
|
export interface PrintOptions {
|
|
327
331
|
title?: string;
|
|
328
332
|
selectedRows?: (string | number)[];
|
|
329
|
-
aggregationResult?: Record<string,
|
|
333
|
+
aggregationResult?: Record<string, unknown> | null;
|
|
330
334
|
aggregationModel?: GridAggregationModel | null;
|
|
331
335
|
}
|
|
332
336
|
|
|
@@ -376,7 +380,7 @@ export async function printGrid<R extends GridRowModel>(
|
|
|
376
380
|
try {
|
|
377
381
|
let title = 'Print';
|
|
378
382
|
let selectedRows: (string | number)[] | undefined;
|
|
379
|
-
let aggregationResult: Record<string,
|
|
383
|
+
let aggregationResult: Record<string, unknown> | null = null;
|
|
380
384
|
let aggregationModel: GridAggregationModel | null = null;
|
|
381
385
|
|
|
382
386
|
if (typeof titleOrOptions === 'string') {
|
|
@@ -447,7 +451,7 @@ export async function printGrid<R extends GridRowModel>(
|
|
|
447
451
|
rowsToExport.forEach(row => {
|
|
448
452
|
html += '<tr>';
|
|
449
453
|
exportColumns.forEach(col => {
|
|
450
|
-
let value:
|
|
454
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
451
455
|
|
|
452
456
|
if (col.valueGetter) {
|
|
453
457
|
value = col.valueGetter({ row, field: col.field, value });
|
|
@@ -529,10 +533,11 @@ export async function printGrid<R extends GridRowModel>(
|
|
|
529
533
|
}
|
|
530
534
|
}, 500);
|
|
531
535
|
|
|
532
|
-
} catch (e
|
|
536
|
+
} catch (e) {
|
|
533
537
|
console.error('Print generation failed:', e);
|
|
534
538
|
if (printWindow && !printWindow.closed) {
|
|
535
|
-
|
|
539
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
540
|
+
printWindow.document.body.innerHTML = `<div style="color: red; padding: 20px;"><h3>Error Generating Print Preview</h3><p>${message}</p></div>`;
|
|
536
541
|
}
|
|
537
542
|
}
|
|
538
543
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { filterRows, applyFilterItem, applyQuickFilter, FILTER_OPERATORS } from './index';
|
|
3
|
+
import type { GridFilterItem } from '../../types';
|
|
4
|
+
|
|
5
|
+
const ROWS = [
|
|
6
|
+
{ id: 1, name: 'Alice', age: 30, active: true, department: 'Engineering' },
|
|
7
|
+
{ id: 2, name: 'Bob', age: 25, active: false, department: 'Marketing' },
|
|
8
|
+
{ id: 3, name: 'Charlie', age: 35, active: true, department: 'Engineering' },
|
|
9
|
+
{ id: 4, name: 'Diana', age: null, active: false, department: null },
|
|
10
|
+
{ id: 5, name: 'Eve', age: 28, active: true, department: 'Marketing' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe('FILTER_OPERATORS', () => {
|
|
14
|
+
describe('contains', () => {
|
|
15
|
+
it('matches substring case-insensitively', () => {
|
|
16
|
+
expect(FILTER_OPERATORS.contains('Alice', 'ali')).toBe(true);
|
|
17
|
+
expect(FILTER_OPERATORS.contains('Alice', 'Bob')).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it('returns false for null values', () => {
|
|
20
|
+
expect(FILTER_OPERATORS.contains(null, 'x')).toBe(false);
|
|
21
|
+
expect(FILTER_OPERATORS.contains('x', null)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('equals', () => {
|
|
26
|
+
it('matches case-insensitively', () => {
|
|
27
|
+
expect(FILTER_OPERATORS.equals('Alice', 'alice')).toBe(true);
|
|
28
|
+
expect(FILTER_OPERATORS.equals('Alice', 'Bob')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it('returns true for both null', () => {
|
|
31
|
+
expect(FILTER_OPERATORS.equals(null, null)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('startsWith / endsWith', () => {
|
|
36
|
+
it('startsWith matches prefix', () => {
|
|
37
|
+
expect(FILTER_OPERATORS.startsWith('Alice', 'Ali')).toBe(true);
|
|
38
|
+
expect(FILTER_OPERATORS.startsWith('Alice', 'ice')).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it('endsWith matches suffix', () => {
|
|
41
|
+
expect(FILTER_OPERATORS.endsWith('Alice', 'ice')).toBe(true);
|
|
42
|
+
expect(FILTER_OPERATORS.endsWith('Alice', 'Ali')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('isEmpty / isNotEmpty', () => {
|
|
47
|
+
it('isEmpty detects null, undefined, empty string, whitespace', () => {
|
|
48
|
+
expect(FILTER_OPERATORS.isEmpty(null)).toBe(true);
|
|
49
|
+
expect(FILTER_OPERATORS.isEmpty(undefined)).toBe(true);
|
|
50
|
+
expect(FILTER_OPERATORS.isEmpty('')).toBe(true);
|
|
51
|
+
expect(FILTER_OPERATORS.isEmpty(' ')).toBe(true);
|
|
52
|
+
expect(FILTER_OPERATORS.isEmpty('x')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('isNotEmpty is the inverse', () => {
|
|
55
|
+
expect(FILTER_OPERATORS.isNotEmpty('x')).toBe(true);
|
|
56
|
+
expect(FILTER_OPERATORS.isNotEmpty(null)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('numeric comparisons', () => {
|
|
61
|
+
it('> returns true when value exceeds filter', () => {
|
|
62
|
+
expect(FILTER_OPERATORS['>'](30, 25)).toBe(true);
|
|
63
|
+
expect(FILTER_OPERATORS['>'](25, 30)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it('>= includes equality', () => {
|
|
66
|
+
expect(FILTER_OPERATORS['>='](30, 30)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('< and <= work symmetrically', () => {
|
|
69
|
+
expect(FILTER_OPERATORS['<'](20, 30)).toBe(true);
|
|
70
|
+
expect(FILTER_OPERATORS['<='](30, 30)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('returns false for non-numeric values', () => {
|
|
73
|
+
expect(FILTER_OPERATORS['>'](NaN, 10)).toBe(false);
|
|
74
|
+
expect(FILTER_OPERATORS['>'](10, NaN)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('isAnyOf', () => {
|
|
79
|
+
it('matches any value in the array', () => {
|
|
80
|
+
expect(FILTER_OPERATORS.isAnyOf('Engineering', ['Engineering', 'Finance'])).toBe(true);
|
|
81
|
+
expect(FILTER_OPERATORS.isAnyOf('HR', ['Engineering', 'Finance'])).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
it('returns false for non-array filterValue', () => {
|
|
84
|
+
expect(FILTER_OPERATORS.isAnyOf('x', null as unknown as [])).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('applyFilterItem', () => {
|
|
90
|
+
it('returns true for matching field/operator/value', () => {
|
|
91
|
+
const row = { id: 1, name: 'Alice', age: 30 };
|
|
92
|
+
expect(applyFilterItem(row, { field: 'name', operator: 'contains', value: 'ali' })).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns false for non-matching', () => {
|
|
96
|
+
const row = { id: 1, name: 'Alice', age: 30 };
|
|
97
|
+
expect(applyFilterItem(row, { field: 'name', operator: 'contains', value: 'bob' })).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns true and warns for unknown operator', () => {
|
|
101
|
+
const row = { id: 1, name: 'Alice' };
|
|
102
|
+
// Unknown operator → falls through to true (lenient)
|
|
103
|
+
expect(applyFilterItem(row, { field: 'name', operator: 'unknownOp', value: 'x' } as unknown as GridFilterItem)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('applyQuickFilter', () => {
|
|
108
|
+
it('returns true when empty quickFilterValues', () => {
|
|
109
|
+
expect(applyQuickFilter({ id: 1, name: 'Alice' }, [])).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it('matches all terms (AND logic)', () => {
|
|
112
|
+
const row = { id: 1, name: 'Alice', department: 'Engineering' };
|
|
113
|
+
expect(applyQuickFilter(row, ['alice', 'eng'])).toBe(true);
|
|
114
|
+
expect(applyQuickFilter(row, ['alice', 'marketing'])).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
it('ignores null field values', () => {
|
|
117
|
+
expect(applyQuickFilter({ id: 1, name: null }, ['x'])).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('filterRows', () => {
|
|
122
|
+
it('returns all rows when filterModel is empty', () => {
|
|
123
|
+
const result = filterRows(ROWS, { items: [] });
|
|
124
|
+
expect(result).toHaveLength(ROWS.length);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('filters by single contains item', () => {
|
|
128
|
+
const result = filterRows(ROWS, {
|
|
129
|
+
items: [{ field: 'name', operator: 'contains', value: 'a' }],
|
|
130
|
+
});
|
|
131
|
+
// Alice, Charlie, Diana
|
|
132
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['Alice', 'Charlie', 'Diana']));
|
|
133
|
+
expect(result).toHaveLength(3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('applies AND logic across multiple items', () => {
|
|
137
|
+
const result = filterRows(ROWS, {
|
|
138
|
+
items: [
|
|
139
|
+
{ field: 'department', operator: 'equals', value: 'Engineering' },
|
|
140
|
+
{ field: 'age', operator: '>', value: 30 },
|
|
141
|
+
],
|
|
142
|
+
logicOperator: 'and',
|
|
143
|
+
});
|
|
144
|
+
expect(result.map(r => r.name)).toEqual(['Charlie']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('applies OR logic across multiple items', () => {
|
|
148
|
+
const result = filterRows(ROWS, {
|
|
149
|
+
items: [
|
|
150
|
+
{ field: 'name', operator: 'equals', value: 'Alice' },
|
|
151
|
+
{ field: 'name', operator: 'equals', value: 'Eve' },
|
|
152
|
+
],
|
|
153
|
+
logicOperator: 'or',
|
|
154
|
+
});
|
|
155
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['Alice', 'Eve']));
|
|
156
|
+
expect(result).toHaveLength(2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('applies quickFilterValues (global search)', () => {
|
|
160
|
+
const result = filterRows(ROWS, {
|
|
161
|
+
items: [],
|
|
162
|
+
quickFilterValues: ['engineering'],
|
|
163
|
+
});
|
|
164
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['Alice', 'Charlie']));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('handles null field values without throwing', () => {
|
|
168
|
+
const result = filterRows(ROWS, {
|
|
169
|
+
items: [{ field: 'department', operator: 'isEmpty' }],
|
|
170
|
+
});
|
|
171
|
+
expect(result.map(r => r.name)).toEqual(['Diana']);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns same reference when no filter items and no quickfilter', () => {
|
|
175
|
+
const result = filterRows(ROWS, { items: [], quickFilterValues: [] });
|
|
176
|
+
expect(result).toBe(ROWS);
|
|
177
|
+
});
|
|
178
|
+
});
|