@cdx-ui/data-grid 0.3.11 → 0.3.13

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 (96) hide show
  1. package/README.md +3 -1
  2. package/coverage.svg +1 -1
  3. package/lib/commonjs/DataGrid.js +490 -81
  4. package/lib/commonjs/DataGrid.js.map +1 -1
  5. package/lib/commonjs/DataGridFilterPanel.js +547 -0
  6. package/lib/commonjs/DataGridFilterPanel.js.map +1 -0
  7. package/lib/commonjs/adapter/columnTypes.js +256 -0
  8. package/lib/commonjs/adapter/columnTypes.js.map +1 -0
  9. package/lib/commonjs/adapter/columns.js +140 -4
  10. package/lib/commonjs/adapter/columns.js.map +1 -1
  11. package/lib/commonjs/adapter/filterOperators.js +422 -0
  12. package/lib/commonjs/adapter/filterOperators.js.map +1 -0
  13. package/lib/commonjs/adapter/pinning.js +105 -0
  14. package/lib/commonjs/adapter/pinning.js.map +1 -0
  15. package/lib/commonjs/adapter/selectColumn.js +11 -0
  16. package/lib/commonjs/adapter/selectColumn.js.map +1 -1
  17. package/lib/commonjs/adapter/treeData.js +195 -0
  18. package/lib/commonjs/adapter/treeData.js.map +1 -0
  19. package/lib/commonjs/index.js +26 -0
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/numberCells.js +280 -0
  22. package/lib/commonjs/numberCells.js.map +1 -0
  23. package/lib/commonjs/styles.js +107 -1
  24. package/lib/commonjs/styles.js.map +1 -1
  25. package/lib/commonjs/types.js +39 -1
  26. package/lib/commonjs/types.js.map +1 -1
  27. package/lib/commonjs/useDataGridTable.js +234 -18
  28. package/lib/commonjs/useDataGridTable.js.map +1 -1
  29. package/lib/module/DataGrid.js +497 -88
  30. package/lib/module/DataGrid.js.map +1 -1
  31. package/lib/module/DataGridFilterPanel.js +540 -0
  32. package/lib/module/DataGridFilterPanel.js.map +1 -0
  33. package/lib/module/adapter/columnTypes.js +240 -0
  34. package/lib/module/adapter/columnTypes.js.map +1 -0
  35. package/lib/module/adapter/columns.js +137 -4
  36. package/lib/module/adapter/columns.js.map +1 -1
  37. package/lib/module/adapter/filterOperators.js +406 -0
  38. package/lib/module/adapter/filterOperators.js.map +1 -0
  39. package/lib/module/adapter/pinning.js +94 -0
  40. package/lib/module/adapter/pinning.js.map +1 -0
  41. package/lib/module/adapter/selectColumn.js +11 -1
  42. package/lib/module/adapter/selectColumn.js.map +1 -1
  43. package/lib/module/adapter/treeData.js +187 -0
  44. package/lib/module/adapter/treeData.js.map +1 -0
  45. package/lib/module/index.js +2 -0
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/numberCells.js +275 -0
  48. package/lib/module/numberCells.js.map +1 -0
  49. package/lib/module/styles.js +106 -0
  50. package/lib/module/styles.js.map +1 -1
  51. package/lib/module/types.js +38 -0
  52. package/lib/module/types.js.map +1 -1
  53. package/lib/module/useDataGridTable.js +238 -22
  54. package/lib/module/useDataGridTable.js.map +1 -1
  55. package/lib/typescript/DataGrid.d.ts.map +1 -1
  56. package/lib/typescript/DataGridFilterPanel.d.ts +52 -0
  57. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -0
  58. package/lib/typescript/adapter/columnTypes.d.ts +32 -0
  59. package/lib/typescript/adapter/columnTypes.d.ts.map +1 -0
  60. package/lib/typescript/adapter/columns.d.ts +14 -5
  61. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  62. package/lib/typescript/adapter/filterOperators.d.ts +43 -0
  63. package/lib/typescript/adapter/filterOperators.d.ts.map +1 -0
  64. package/lib/typescript/adapter/pinning.d.ts +26 -0
  65. package/lib/typescript/adapter/pinning.d.ts.map +1 -0
  66. package/lib/typescript/adapter/selectColumn.d.ts +8 -0
  67. package/lib/typescript/adapter/selectColumn.d.ts.map +1 -1
  68. package/lib/typescript/adapter/treeData.d.ts +36 -0
  69. package/lib/typescript/adapter/treeData.d.ts.map +1 -0
  70. package/lib/typescript/index.d.ts +4 -1
  71. package/lib/typescript/index.d.ts.map +1 -1
  72. package/lib/typescript/numberCells.d.ts +46 -0
  73. package/lib/typescript/numberCells.d.ts.map +1 -0
  74. package/lib/typescript/styles.d.ts +32 -0
  75. package/lib/typescript/styles.d.ts.map +1 -1
  76. package/lib/typescript/types.d.ts +154 -7
  77. package/lib/typescript/types.d.ts.map +1 -1
  78. package/lib/typescript/useDataGridTable.d.ts +40 -4
  79. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  80. package/package.json +5 -5
  81. package/src/DataGrid.tsx +745 -95
  82. package/src/DataGridFilterPanel.tsx +648 -0
  83. package/src/adapter/columnTypes.ts +327 -0
  84. package/src/adapter/columns.test.ts +75 -1
  85. package/src/adapter/columns.ts +192 -3
  86. package/src/adapter/filterOperators.test.ts +292 -0
  87. package/src/adapter/filterOperators.ts +524 -0
  88. package/src/adapter/pinning.ts +134 -0
  89. package/src/adapter/selectColumn.ts +12 -1
  90. package/src/adapter/treeData.ts +228 -0
  91. package/src/index.ts +19 -0
  92. package/src/numberCells.tsx +379 -0
  93. package/src/styles.ts +131 -0
  94. package/src/types.ts +190 -7
  95. package/src/useDataGridTable.test.ts +69 -0
  96. package/src/useDataGridTable.ts +338 -17
@@ -0,0 +1,292 @@
1
+ import type { GridColDef, GridFilterOperator } from '../types';
2
+ import {
3
+ coerceFilterInput,
4
+ filterValueToInputText,
5
+ getDefaultFilterOperator,
6
+ getFilterOperatorLabel,
7
+ getGridFilterOperators,
8
+ headerFilterValueFromText,
9
+ matchesOperatorFilter,
10
+ } from './filterOperators';
11
+
12
+ type Row = Record<string, unknown>;
13
+
14
+ function matches(
15
+ col: GridColDef<Row>,
16
+ cell: unknown,
17
+ operator: GridFilterOperator,
18
+ value?: unknown,
19
+ ) {
20
+ return matchesOperatorFilter(col, cell, {}, { operator, value });
21
+ }
22
+
23
+ const text: GridColDef<Row> = { field: 'name', type: 'string' };
24
+ const untyped: GridColDef<Row> = { field: 'name' };
25
+ const num: GridColDef<Row> = { field: 'amount', type: 'number' };
26
+ const date: GridColDef<Row> = { field: 'opened', type: 'date' };
27
+ const dateTime: GridColDef<Row> = { field: 'at', type: 'dateTime' };
28
+ const bool: GridColDef<Row> = { field: 'active', type: 'boolean' };
29
+ const select: GridColDef<Row> = {
30
+ field: 'status',
31
+ type: 'singleSelect',
32
+ valueOptions: [
33
+ { value: 'a', label: 'Active' },
34
+ { value: 'p', label: 'Pending' },
35
+ ],
36
+ };
37
+
38
+ describe('getGridFilterOperators', () => {
39
+ it('lists the MUI operators per column type', () => {
40
+ expect(getGridFilterOperators('string')).toEqual([
41
+ 'contains',
42
+ 'doesNotContain',
43
+ 'equals',
44
+ 'doesNotEqual',
45
+ 'startsWith',
46
+ 'endsWith',
47
+ 'isEmpty',
48
+ 'isNotEmpty',
49
+ 'isAnyOf',
50
+ ]);
51
+ expect(getGridFilterOperators('number')).toEqual([
52
+ '=',
53
+ '!=',
54
+ '>',
55
+ '>=',
56
+ '<',
57
+ '<=',
58
+ 'isEmpty',
59
+ 'isNotEmpty',
60
+ 'isAnyOf',
61
+ ]);
62
+ expect(getGridFilterOperators('date')).toEqual(getGridFilterOperators('dateTime'));
63
+ expect(getGridFilterOperators('date')).toEqual([
64
+ 'is',
65
+ 'not',
66
+ 'after',
67
+ 'onOrAfter',
68
+ 'before',
69
+ 'onOrBefore',
70
+ 'isEmpty',
71
+ 'isNotEmpty',
72
+ ]);
73
+ expect(getGridFilterOperators('boolean')).toEqual(['is']);
74
+ expect(getGridFilterOperators('singleSelect')).toEqual(['is', 'not', 'isAnyOf']);
75
+ expect(getGridFilterOperators(undefined)).toEqual(getGridFilterOperators('string'));
76
+ });
77
+
78
+ it('preselects contains, =, and is', () => {
79
+ expect(getDefaultFilterOperator('string')).toBe('contains');
80
+ expect(getDefaultFilterOperator('number')).toBe('=');
81
+ expect(getDefaultFilterOperator('date')).toBe('is');
82
+ expect(getDefaultFilterOperator('boolean')).toBe('is');
83
+ expect(getDefaultFilterOperator('singleSelect')).toBe('is');
84
+ });
85
+ });
86
+
87
+ describe('matchesOperatorFilter', () => {
88
+ describe('text', () => {
89
+ it.each([
90
+ ['contains', 'ali', true],
91
+ ['contains', 'bob', false],
92
+ ['doesNotContain', 'bob', true],
93
+ ['doesNotContain', 'ali', false],
94
+ ['equals', 'ALICE', true],
95
+ ['equals', 'ali', false],
96
+ ['doesNotEqual', 'ali', true],
97
+ ['startsWith', 'AL', true],
98
+ ['startsWith', 'ice', false],
99
+ ['endsWith', 'ICE', true],
100
+ ['endsWith', 'al', false],
101
+ ] as const)('%s %s on "Alice" is %s', (operator, value, expected) => {
102
+ expect(matches(text, 'Alice', operator, value)).toBe(expected);
103
+ expect(matches(untyped, 'Alice', operator, value)).toBe(expected);
104
+ });
105
+
106
+ it('treats a null cell as empty text', () => {
107
+ expect(matches(text, null, 'contains', 'a')).toBe(false);
108
+ expect(matches(text, null, 'doesNotContain', 'a')).toBe(true);
109
+ });
110
+
111
+ it('matches isAnyOf against any listed value exactly', () => {
112
+ expect(matches(text, 'Alice', 'isAnyOf', ['Bob', 'Alice'])).toBe(true);
113
+ expect(matches(text, 'Alice', 'isAnyOf', ['Bob', 'Carol'])).toBe(false);
114
+ expect(matches(text, 'Alice', 'isAnyOf', 'Alice')).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe('isEmpty / isNotEmpty', () => {
119
+ it.each([
120
+ [text, null, true],
121
+ [text, '', true],
122
+ [text, 'x', false],
123
+ [num, undefined, true],
124
+ [num, Number.NaN, true],
125
+ [num, 0, false],
126
+ [date, null, true],
127
+ [date, new Date(2026, 0, 1), false],
128
+ ] as const)('isEmpty on %# is %s', (col, cell, expected) => {
129
+ expect(matches(col as GridColDef<Row>, cell, 'isEmpty')).toBe(expected);
130
+ expect(matches(col as GridColDef<Row>, cell, 'isNotEmpty')).toBe(!expected);
131
+ });
132
+ });
133
+
134
+ describe('number', () => {
135
+ it.each([
136
+ ['=', 100, true],
137
+ ['=', 99, false],
138
+ ['!=', 99, true],
139
+ ['!=', 100, false],
140
+ ['>', 99, true],
141
+ ['>', 100, false],
142
+ ['>=', 100, true],
143
+ ['<', 101, true],
144
+ ['<', 100, false],
145
+ ['<=', 100, true],
146
+ ['<=', 99, false],
147
+ ] as const)('100 %s %s is %s', (operator, operand, expected) => {
148
+ expect(matches(num, 100, operator, operand)).toBe(expected);
149
+ });
150
+
151
+ it('coerces numeric strings and ignores unparseable operands', () => {
152
+ expect(matches(num, 100, '>', '50')).toBe(true);
153
+ expect(matches(num, 100, '>', 'abc')).toBe(true);
154
+ expect(matches(num, 100, '>', '')).toBe(true);
155
+ });
156
+
157
+ it('keeps null cells for != only', () => {
158
+ expect(matches(num, null, '=', 1)).toBe(false);
159
+ expect(matches(num, null, '>', 1)).toBe(false);
160
+ expect(matches(num, null, '!=', 1)).toBe(true);
161
+ });
162
+
163
+ it('supports isAnyOf', () => {
164
+ expect(matches(num, 2, 'isAnyOf', [1, '2'])).toBe(true);
165
+ expect(matches(num, 3, 'isAnyOf', [1, 2])).toBe(false);
166
+ });
167
+ });
168
+
169
+ describe('date', () => {
170
+ const cell = new Date(2026, 5, 15, 13, 45);
171
+
172
+ it.each([
173
+ ['is', new Date(2026, 5, 15, 1, 0), true],
174
+ ['is', new Date(2026, 5, 16), false],
175
+ ['not', new Date(2026, 5, 16), true],
176
+ ['after', new Date(2026, 5, 14), true],
177
+ ['after', new Date(2026, 5, 15), false],
178
+ ['onOrAfter', new Date(2026, 5, 15), true],
179
+ ['before', new Date(2026, 5, 16), true],
180
+ ['before', new Date(2026, 5, 15), false],
181
+ ['onOrBefore', new Date(2026, 5, 15), true],
182
+ ['onOrBefore', new Date(2026, 5, 14), false],
183
+ ] as const)('%s %s compares by calendar day', (operator, operand, expected) => {
184
+ expect(matches(date, cell, operator, operand)).toBe(expected);
185
+ });
186
+
187
+ it('parses date-only strings in local time', () => {
188
+ expect(matches(date, new Date(2026, 0, 1), 'is', '2026-01-01')).toBe(true);
189
+ expect(matches(date, new Date(2026, 0, 1), 'before', '2026-01-02')).toBe(true);
190
+ });
191
+
192
+ it('compares dateTime to the millisecond', () => {
193
+ const at = new Date(2026, 5, 15, 13, 45);
194
+ expect(matches(dateTime, at, 'is', new Date(at))).toBe(true);
195
+ expect(matches(dateTime, at, 'is', new Date(2026, 5, 15))).toBe(false);
196
+ expect(matches(dateTime, at, 'after', new Date(2026, 5, 15))).toBe(true);
197
+ });
198
+
199
+ it('ignores invalid operands and keeps null cells for not only', () => {
200
+ expect(matches(date, cell, 'is', 'not a date')).toBe(true);
201
+ expect(matches(date, null, 'is', new Date(2026, 5, 15))).toBe(false);
202
+ expect(matches(date, null, 'not', new Date(2026, 5, 15))).toBe(true);
203
+ });
204
+ });
205
+
206
+ describe('boolean', () => {
207
+ it('matches is against booleans and "true" / "false"', () => {
208
+ expect(matches(bool, true, 'is', true)).toBe(true);
209
+ expect(matches(bool, true, 'is', false)).toBe(false);
210
+ expect(matches(bool, false, 'is', 'false')).toBe(true);
211
+ expect(matches(bool, null, 'is', false)).toBe(false);
212
+ });
213
+
214
+ it('filters nothing for a blank operand', () => {
215
+ expect(matches(bool, true, 'is', '')).toBe(true);
216
+ expect(matches(bool, false, 'is', undefined)).toBe(true);
217
+ });
218
+ });
219
+
220
+ describe('singleSelect', () => {
221
+ it('compares stored values, not labels', () => {
222
+ expect(matches(select, 'a', 'is', 'a')).toBe(true);
223
+ expect(matches(select, 'a', 'is', 'Active')).toBe(false);
224
+ expect(matches(select, 'a', 'not', 'p')).toBe(true);
225
+ expect(matches(select, null, 'not', 'p')).toBe(true);
226
+ });
227
+
228
+ it('supports isAnyOf and numeric option values', () => {
229
+ expect(matches(select, 'p', 'isAnyOf', ['a', 'p'])).toBe(true);
230
+ expect(matches(select, 'p', 'isAnyOf', ['a'])).toBe(false);
231
+ expect(matches(select, 2, 'is', '2')).toBe(true);
232
+ });
233
+ });
234
+
235
+ it('filters nothing for blank operands and for operators the type does not accept', () => {
236
+ expect(matches(text, 'Alice', 'contains', '')).toBe(true);
237
+ expect(matches(text, 'Alice', 'isAnyOf', [])).toBe(true);
238
+ expect(matches(text, 'Alice', '>', 5)).toBe(true);
239
+ expect(matches(num, 5, 'contains', 'x')).toBe(true);
240
+ });
241
+ });
242
+
243
+ describe('header filter text', () => {
244
+ it('labels operators the way the menu shows them', () => {
245
+ expect(getFilterOperatorLabel('doesNotContain')).toBe('does not contain');
246
+ expect(getFilterOperatorLabel('>=')).toBe('>=');
247
+ expect(getFilterOperatorLabel('onOrBefore')).toBe('is on or before');
248
+ });
249
+
250
+ it('splits isAnyOf on commas and types numbers', () => {
251
+ expect(headerFilterValueFromText(text, 'isAnyOf', ' Bob, Alice ')).toEqual(['Bob', 'Alice']);
252
+ expect(headerFilterValueFromText(num, 'isAnyOf', '50, 1.50, 2')).toEqual([50, '1.50', 2]);
253
+ expect(headerFilterValueFromText(num, '>', '100')).toBe(100);
254
+ expect(headerFilterValueFromText(num, 'isEmpty', '100')).toBeUndefined();
255
+ expect(headerFilterValueFromText(select, 'is', 'Pending')).toBe('p');
256
+ expect(headerFilterValueFromText(select, 'isAnyOf', 'Active, p')).toEqual(['a', 'p']);
257
+ });
258
+ });
259
+
260
+ describe('coerceFilterInput', () => {
261
+ it('types numbers, dates, and booleans that round-trip', () => {
262
+ expect(coerceFilterInput(num, '>', '100')).toBe(100);
263
+ expect(coerceFilterInput(num, '>', '1.5')).toBe(1.5);
264
+ expect(coerceFilterInput(date, 'after', '2026-01-02')).toEqual(new Date(2026, 0, 2));
265
+ expect(coerceFilterInput(bool, 'is', 'true')).toBe(true);
266
+ });
267
+
268
+ it('keeps text that would be rewritten under the cursor', () => {
269
+ expect(coerceFilterInput(num, '>', '1.')).toBe('1.');
270
+ expect(coerceFilterInput(num, '>', '1.50')).toBe('1.50');
271
+ expect(coerceFilterInput(num, '>', '-')).toBe('-');
272
+ expect(coerceFilterInput(date, 'after', '2026-01')).toBe('2026-01');
273
+ });
274
+
275
+ it('keeps text for isAnyOf, text columns, and untyped columns', () => {
276
+ expect(coerceFilterInput(num, 'isAnyOf', '1')).toBe('1');
277
+ expect(coerceFilterInput(text, 'contains', '12')).toBe('12');
278
+ expect(coerceFilterInput(undefined, 'contains', '12')).toBe('12');
279
+ });
280
+ });
281
+
282
+ describe('filterValueToInputText', () => {
283
+ it('renders operands back as input text', () => {
284
+ expect(filterValueToInputText(undefined)).toBe('');
285
+ expect(filterValueToInputText('abc')).toBe('abc');
286
+ expect(filterValueToInputText(12.5)).toBe('12.5');
287
+ expect(filterValueToInputText(false)).toBe('false');
288
+ expect(filterValueToInputText(new Date(2026, 0, 2))).toBe('2026-01-02');
289
+ expect(filterValueToInputText(['a', 2])).toBe('a, 2');
290
+ expect(filterValueToInputText({})).toBe('');
291
+ });
292
+ });