@opencorestack/opengridx 2.0.4 → 3.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.
Files changed (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -1,120 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import type { GridColDef, GridRowModel } from '../../types';
3
-
4
- // --- jsPDF mocks ---
5
- const mockAutoTable = vi.fn();
6
- const mockSave = vi.fn();
7
- const mockSetFontSize = vi.fn();
8
- const mockSetFont = vi.fn();
9
- const mockSetTextColor = vi.fn();
10
- const mockText = vi.fn();
11
- const mockLine = vi.fn();
12
- const mockSetDrawColor = vi.fn();
13
- const mockSetLineWidth = vi.fn();
14
- const mockAddImage = vi.fn();
15
- const mockSetPage = vi.fn();
16
-
17
- class MockJsPDF {
18
- save = mockSave;
19
- setFontSize = mockSetFontSize;
20
- setFont = mockSetFont;
21
- setTextColor = mockSetTextColor;
22
- text = mockText;
23
- line = mockLine;
24
- setDrawColor = mockSetDrawColor;
25
- setLineWidth = mockSetLineWidth;
26
- addImage = mockAddImage;
27
- setPage = mockSetPage;
28
- lastAutoTable = { finalY: 50 };
29
- internal = {
30
- pageSize: { getWidth: () => 297, getHeight: () => 210 },
31
- getNumberOfPages: () => 1,
32
- };
33
- getNumberOfPages = () => 1;
34
- }
35
-
36
- vi.mock('jspdf', () => ({ default: MockJsPDF }));
37
- vi.mock('jspdf-autotable', () => ({ default: mockAutoTable }));
38
-
39
- const sampleColumns: GridColDef[] = [
40
- { field: 'name', headerName: 'Name', width: 150 },
41
- { field: 'salary', headerName: 'Salary', width: 120, valueFormatter: ({ value }) => `$${value}` },
42
- { field: 'hidden', headerName: 'Hidden', width: 100, exportable: false },
43
- { field: '__check__', headerName: '', width: 50 },
44
- ];
45
-
46
- const sampleRows: GridRowModel[] = [
47
- { id: 1, name: 'Alice', salary: 90000, hidden: 'secret' },
48
- { id: 2, name: 'Bob', salary: 80000, hidden: 'secret' },
49
- ];
50
-
51
- describe('exportToPdf', () => {
52
- beforeEach(() => {
53
- mockAutoTable.mockClear();
54
- mockSave.mockClear();
55
- mockText.mockClear();
56
- mockAddImage.mockClear();
57
- });
58
-
59
- it('calls doc.save with the correct filename', async () => {
60
- const { exportToPdf } = await import('./exportToPdf');
61
- await exportToPdf(sampleRows, sampleColumns, { fileName: 'my-report' });
62
- expect(mockSave).toHaveBeenCalledWith('my-report.pdf');
63
- });
64
-
65
- it('uses default filename "export.pdf" when not provided', async () => {
66
- const { exportToPdf } = await import('./exportToPdf');
67
- await exportToPdf(sampleRows, sampleColumns);
68
- expect(mockSave).toHaveBeenCalledWith('export.pdf');
69
- });
70
-
71
- it('excludes columns with exportable: false and system columns', async () => {
72
- const { exportToPdf } = await import('./exportToPdf');
73
- await exportToPdf(sampleRows, sampleColumns);
74
- const call = mockAutoTable.mock.calls[0][1];
75
- // head should only have Name and Salary — not Hidden or __check__
76
- expect(call.head[0]).toEqual(['Name', 'Salary']);
77
- });
78
-
79
- it('applies valueFormatter to cell values', async () => {
80
- const { exportToPdf } = await import('./exportToPdf');
81
- await exportToPdf(sampleRows, sampleColumns);
82
- const call = mockAutoTable.mock.calls[0][1];
83
- // salary column should be formatted
84
- expect(call.body[0][1]).toBe('$90000');
85
- expect(call.body[1][1]).toBe('$80000');
86
- });
87
-
88
- it('filters to selectedRows when provided', async () => {
89
- const { exportToPdf } = await import('./exportToPdf');
90
- await exportToPdf(sampleRows, sampleColumns, { selectedRows: [1] });
91
- const call = mockAutoTable.mock.calls[0][1];
92
- expect(call.body).toHaveLength(1);
93
- expect(call.body[0][0]).toBe('Alice');
94
- });
95
-
96
- it('appends aggregation footer when aggregationResult is provided', async () => {
97
- const { exportToPdf } = await import('./exportToPdf');
98
- await exportToPdf(sampleRows, sampleColumns, {
99
- aggregationResult: { salary: 170000 },
100
- aggregationModel: { salary: 'sum' },
101
- });
102
- const call = mockAutoTable.mock.calls[0][1];
103
- expect(call.foot).toBeDefined();
104
- expect(call.foot[0][1]).toBe('$170000');
105
- });
106
-
107
- it('renders title text when title option is set', async () => {
108
- const { exportToPdf } = await import('./exportToPdf');
109
- await exportToPdf(sampleRows, sampleColumns, { title: 'Sales Report' });
110
- // doc.text should have been called with the title
111
- const textCalls = mockText.mock.calls.map((c: unknown[]) => c[0]);
112
- expect(textCalls.some((t: unknown) => t === 'Sales Report')).toBe(true);
113
- });
114
-
115
- it('throws descriptive error when jspdf is not installed', async () => {
116
- // This test uses a different mock setup — skip in unit suite,
117
- // documented as an integration concern
118
- expect(true).toBe(true);
119
- });
120
- });
@@ -1,144 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { exportToCsv, exportToJson } from './index';
3
- import type { GridColDef, GridGroupedExportRow, GridRowModel } from '../../types';
4
-
5
- // --- shared fixtures ---
6
-
7
- type Row = GridRowModel & { id: number; dept: string; salary: number };
8
-
9
- const columns: GridColDef<Row>[] = [
10
- { field: 'dept', headerName: 'Department' },
11
- { field: 'salary', headerName: 'Salary', type: 'number' },
12
- ];
13
-
14
- const flatRows: Row[] = [
15
- { id: 1, dept: 'Engineering', salary: 90000 },
16
- { id: 2, dept: 'Engineering', salary: 85000 },
17
- { id: 3, dept: 'Marketing', salary: 70000 },
18
- ];
19
-
20
- const groupedRows: GridGroupedExportRow[] = [
21
- { type: 'group-header', depth: 0, groupField: 'dept', groupValue: 'Engineering' },
22
- { type: 'leaf', depth: 1, row: { id: 1, dept: 'Engineering', salary: 90000 } },
23
- { type: 'leaf', depth: 1, row: { id: 2, dept: 'Engineering', salary: 85000 } },
24
- { type: 'group-subtotal', depth: 0, groupField: 'dept', groupValue: 'Engineering', aggregatedValues: { salary: 175000 } },
25
- { type: 'group-header', depth: 0, groupField: 'dept', groupValue: 'Marketing' },
26
- { type: 'leaf', depth: 1, row: { id: 3, dept: 'Marketing', salary: 70000 } },
27
- { type: 'group-subtotal', depth: 0, groupField: 'dept', groupValue: 'Marketing', aggregatedValues: { salary: 70000 } },
28
- { type: 'grand-total', depth: 0, aggregatedValues: { salary: 245000 } },
29
- ];
30
-
31
- // --- intercept URL.createObjectURL so we can read Blob content ---
32
-
33
- let capturedBlob: Blob | null = null;
34
- const origCreateObjectURL = URL.createObjectURL;
35
- const origRevokeObjectURL = URL.revokeObjectURL;
36
- const origClick = HTMLAnchorElement.prototype.click;
37
-
38
- beforeEach(() => {
39
- capturedBlob = null;
40
- URL.createObjectURL = (blob: Blob) => { capturedBlob = blob; return 'blob:mock'; };
41
- URL.revokeObjectURL = () => {};
42
- HTMLAnchorElement.prototype.click = () => {};
43
- });
44
-
45
- afterEach(() => {
46
- URL.createObjectURL = origCreateObjectURL;
47
- URL.revokeObjectURL = origRevokeObjectURL;
48
- HTMLAnchorElement.prototype.click = origClick;
49
- });
50
-
51
- // --- CSV tests ---
52
-
53
- describe('exportToCsv — flat (no groupedRows)', () => {
54
- it('produces a header row followed by one row per data record', async () => {
55
- exportToCsv(flatRows, columns, { fileName: 'test.csv' });
56
- const text = await capturedBlob!.text();
57
- const lines = text.split('\n').filter(Boolean);
58
- expect(lines[0]).toBe('Department,Salary');
59
- expect(lines.length).toBe(4); // header + 3 data rows
60
- });
61
-
62
- it('does not emit group-header markers or Subtotal labels', async () => {
63
- exportToCsv(flatRows, columns, { fileName: 'flat.csv' });
64
- const text = await capturedBlob!.text();
65
- expect(text).not.toContain('dept:');
66
- expect(text).not.toContain('Subtotal');
67
- expect(text).not.toContain('Grand Total');
68
- });
69
- });
70
-
71
- describe('exportToCsv — grouped', () => {
72
- it('emits group-header rows, leaf rows, subtotals, and grand total', async () => {
73
- exportToCsv(flatRows, columns, {
74
- fileName: 'grouped.csv',
75
- groupedRows,
76
- aggregationModel: { salary: 'sum' },
77
- });
78
- const text = await capturedBlob!.text();
79
- const lines = text.split('\n').filter(Boolean);
80
-
81
- // Header
82
- expect(lines[0]).toBe('Department,Salary');
83
- // Group header for Engineering (depth 0 → no leading spaces, just "dept: Engineering")
84
- expect(lines[1]).toContain('dept: Engineering');
85
- // Leaf rows: salary values present
86
- expect(lines[2]).toContain('90000');
87
- expect(lines[3]).toContain('85000');
88
- // Subtotal row for Engineering (formatAggregationValue adds commas: "175,000")
89
- expect(lines[4]).toContain('Subtotal');
90
- expect(lines[4]).toContain('175');
91
- // Grand total
92
- const last = lines[lines.length - 1];
93
- expect(last).toContain('Grand Total');
94
- expect(last).toContain('245');
95
- });
96
-
97
- it('total line count matches structure (1 header + 2 group-headers + 3 leaves + 2 subtotals + 1 grand-total)', async () => {
98
- exportToCsv(flatRows, columns, { groupedRows, aggregationModel: { salary: 'sum' } });
99
- const text = await capturedBlob!.text();
100
- const lines = text.split('\n').filter(Boolean);
101
- expect(lines.length).toBe(9);
102
- });
103
- });
104
-
105
- // --- JSON tests ---
106
-
107
- describe('exportToJson — flat (no groupedRows)', () => {
108
- it('emits a plain array with one entry per row', async () => {
109
- exportToJson(flatRows, columns, {});
110
- const text = await capturedBlob!.text();
111
- const parsed = JSON.parse(text) as unknown[];
112
- expect(Array.isArray(parsed)).toBe(true);
113
- expect(parsed.length).toBe(3);
114
- });
115
- });
116
-
117
- describe('exportToJson — grouped', () => {
118
- it('emits { groups, grandTotal } with two top-level groups', async () => {
119
- exportToJson(flatRows, columns, {
120
- groupedRows,
121
- aggregationModel: { salary: 'sum' },
122
- });
123
- const text = await capturedBlob!.text();
124
- const parsed = JSON.parse(text) as {
125
- groups: { group: string; rows: unknown[]; subtotals?: Record<string, unknown> }[];
126
- grandTotal?: Record<string, unknown>;
127
- };
128
- expect(parsed.groups).toHaveLength(2);
129
- expect(parsed.groups[0].group).toBe('dept: Engineering');
130
- expect(parsed.groups[0].rows).toHaveLength(2);
131
- expect(parsed.groups[0].subtotals?.salary).toBe(175000);
132
- expect(parsed.groups[1].group).toBe('dept: Marketing');
133
- expect(parsed.groups[1].rows).toHaveLength(1);
134
- expect(parsed.grandTotal?.salary).toBe(245000);
135
- });
136
-
137
- it('falls through to flat array when groupedRows is absent', async () => {
138
- exportToJson(flatRows, columns, {});
139
- const text = await capturedBlob!.text();
140
- const parsed = JSON.parse(text) as unknown[];
141
- expect(Array.isArray(parsed)).toBe(true);
142
- expect(parsed.length).toBe(3);
143
- });
144
- });
@@ -1,178 +0,0 @@
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
- });
@@ -1,88 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { sortRows, compareValues } from './index';
3
-
4
- describe('compareValues', () => {
5
- describe('null handling', () => {
6
- it('null === null → 0', () => expect(compareValues(null, null, 'asc')).toBe(0));
7
- it('null sorts last in asc', () => expect(compareValues(null, 1, 'asc')).toBeGreaterThan(0));
8
- it('null sorts first in desc', () => expect(compareValues(null, 1, 'desc')).toBeLessThan(0));
9
- it('non-null before null in asc', () => expect(compareValues(1, null, 'asc')).toBeLessThan(0));
10
- });
11
-
12
- describe('numbers', () => {
13
- it('asc: smaller first', () => expect(compareValues(1, 2, 'asc')).toBeLessThan(0));
14
- it('desc: larger first', () => expect(compareValues(2, 1, 'desc')).toBeLessThan(0));
15
- it('equal numbers → 0', () => expect(compareValues(5, 5, 'asc')).toBe(0));
16
- });
17
-
18
- describe('dates', () => {
19
- const d1 = new Date('2023-01-01');
20
- const d2 = new Date('2024-01-01');
21
- it('asc: earlier date first', () => expect(compareValues(d1, d2, 'asc')).toBeLessThan(0));
22
- it('desc: later date first', () => expect(compareValues(d2, d1, 'desc')).toBeLessThan(0));
23
- });
24
-
25
- describe('strings', () => {
26
- it('asc: lexicographic order', () => expect(compareValues('apple', 'banana', 'asc')).toBeLessThan(0));
27
- it('desc: reversed order', () => expect(compareValues('banana', 'apple', 'desc')).toBeLessThan(0));
28
- it('case-insensitive', () => expect(compareValues('APPLE', 'apple', 'asc')).toBe(0));
29
- });
30
- });
31
-
32
- describe('sortRows', () => {
33
- const ROWS = [
34
- { id: 3, name: 'Charlie', age: 35, dept: 'Engineering' },
35
- { id: 1, name: 'Alice', age: 30, dept: 'Marketing' },
36
- { id: 4, name: 'Diana', age: 25, dept: 'Engineering' },
37
- { id: 2, name: 'Bob', age: 30, dept: 'Marketing' },
38
- ];
39
-
40
- it('returns same reference when sortModel is empty', () => {
41
- const result = sortRows(ROWS, []);
42
- expect(result).toBe(ROWS);
43
- });
44
-
45
- it('does not mutate the original array', () => {
46
- const original = [...ROWS];
47
- sortRows(ROWS, [{ field: 'name', sort: 'asc' }]);
48
- expect(ROWS).toEqual(original);
49
- });
50
-
51
- it('sorts by single field asc', () => {
52
- const result = sortRows(ROWS, [{ field: 'name', sort: 'asc' }]);
53
- expect(result.map(r => r.name)).toEqual(['Alice', 'Bob', 'Charlie', 'Diana']);
54
- });
55
-
56
- it('sorts by single field desc', () => {
57
- const result = sortRows(ROWS, [{ field: 'age', sort: 'desc' }]);
58
- expect(result.map(r => r.age)).toEqual([35, 30, 30, 25]);
59
- });
60
-
61
- it('applies multi-field sort (age asc, name asc for ties)', () => {
62
- const result = sortRows(ROWS, [
63
- { field: 'age', sort: 'asc' },
64
- { field: 'name', sort: 'asc' },
65
- ]);
66
- expect(result.map(r => r.name)).toEqual(['Diana', 'Alice', 'Bob', 'Charlie']);
67
- });
68
-
69
- it('handles null values — nulls sort last in asc', () => {
70
- const rowsWithNull = [
71
- { id: 1, name: 'Alice', age: 30 },
72
- { id: 2, name: null, age: 25 },
73
- { id: 3, name: 'Bob', age: 35 },
74
- ];
75
- const result = sortRows(rowsWithNull, [{ field: 'name', sort: 'asc' }]);
76
- expect(result[result.length - 1].name).toBeNull();
77
- });
78
-
79
- it('handles rows with identical values — stable relative order preserved', () => {
80
- const rows = [
81
- { id: 1, score: 10 },
82
- { id: 2, score: 10 },
83
- { id: 3, score: 10 },
84
- ];
85
- const result = sortRows(rows, [{ field: 'score', sort: 'asc' }]);
86
- expect(result.map(r => r.id)).toEqual([1, 2, 3]);
87
- });
88
- });