@opencorestack/opengridx 2.1.0 → 3.0.1
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 +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -166
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +6 -7
- package/docs/components/datagrid.md +15 -13
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +90 -38
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +126 -39
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +19 -4
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +279 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +104 -5
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +17 -5
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +82 -51
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +55 -5
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -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
|
-
});
|