@prairielearn/ui 1.4.0 → 1.6.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 +12 -0
- package/README.md +60 -2
- package/dist/components/CategoricalColumnFilter.d.ts +1 -1
- package/dist/components/CategoricalColumnFilter.d.ts.map +1 -1
- package/dist/components/CategoricalColumnFilter.js.map +1 -1
- package/dist/components/PresetFilterDropdown.d.ts +19 -0
- package/dist/components/PresetFilterDropdown.d.ts.map +1 -0
- package/dist/components/PresetFilterDropdown.js +93 -0
- package/dist/components/PresetFilterDropdown.js.map +1 -0
- package/dist/components/nuqs.d.ts +52 -0
- package/dist/components/nuqs.d.ts.map +1 -0
- package/dist/components/nuqs.js +212 -0
- package/dist/components/nuqs.js.map +1 -0
- package/dist/components/nuqs.test.d.ts +2 -0
- package/dist/components/nuqs.test.d.ts.map +1 -0
- package/dist/components/nuqs.test.js +231 -0
- package/dist/components/nuqs.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/CategoricalColumnFilter.tsx +1 -1
- package/src/components/PresetFilterDropdown.tsx +155 -0
- package/src/components/nuqs.test.ts +276 -0
- package/src/components/nuqs.tsx +230 -0
- package/src/index.ts +9 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseAsColumnPinningState, parseAsColumnVisibilityStateWithColumns, parseAsNumericFilter, parseAsSortingState, } from './nuqs.js';
|
|
3
|
+
describe('parseAsSortingState', () => {
|
|
4
|
+
describe('parse', () => {
|
|
5
|
+
it('parses valid asc', () => {
|
|
6
|
+
expect(parseAsSortingState.parse('col:asc')).toEqual([{ id: 'col', desc: false }]);
|
|
7
|
+
});
|
|
8
|
+
it('parses valid desc', () => {
|
|
9
|
+
expect(parseAsSortingState.parse('col:desc')).toEqual([{ id: 'col', desc: true }]);
|
|
10
|
+
});
|
|
11
|
+
it('parses multiple columns', () => {
|
|
12
|
+
expect(parseAsSortingState.parse('col1:asc,col2:desc')).toEqual([
|
|
13
|
+
{ id: 'col1', desc: false },
|
|
14
|
+
{ id: 'col2', desc: true },
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
it('ignores invalid columns in multi-column', () => {
|
|
18
|
+
expect(parseAsSortingState.parse('col1:asc,invalid,foo:bar,col2:desc')).toEqual([
|
|
19
|
+
{ id: 'col1', desc: false },
|
|
20
|
+
{ id: 'col2', desc: true },
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
it('returns [] for empty string', () => {
|
|
24
|
+
expect(parseAsSortingState.parse('')).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
it('returns [] for missing id', () => {
|
|
27
|
+
expect(parseAsSortingState.parse(':asc')).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
it('returns [] for invalid direction', () => {
|
|
30
|
+
expect(parseAsSortingState.parse('col:foo')).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
it('returns [] for undefined', () => {
|
|
33
|
+
expect(parseAsSortingState.parse(undefined)).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('serialize', () => {
|
|
37
|
+
it('serializes asc', () => {
|
|
38
|
+
const state = [{ id: 'col', desc: false }];
|
|
39
|
+
expect(parseAsSortingState.serialize(state)).toBe('col:asc');
|
|
40
|
+
});
|
|
41
|
+
it('serializes desc', () => {
|
|
42
|
+
const state = [{ id: 'col', desc: true }];
|
|
43
|
+
expect(parseAsSortingState.serialize(state)).toBe('col:desc');
|
|
44
|
+
});
|
|
45
|
+
it('serializes multiple columns', () => {
|
|
46
|
+
const state = [
|
|
47
|
+
{ id: 'col1', desc: false },
|
|
48
|
+
{ id: 'col2', desc: true },
|
|
49
|
+
];
|
|
50
|
+
expect(parseAsSortingState.serialize(state)).toBe('col1:asc,col2:desc');
|
|
51
|
+
});
|
|
52
|
+
it('serializes empty array as null', () => {
|
|
53
|
+
expect(parseAsSortingState.serialize([])).toBe(null);
|
|
54
|
+
});
|
|
55
|
+
it('serializes missing id as empty string', () => {
|
|
56
|
+
expect(parseAsSortingState.serialize([{ id: '', desc: false }])).toBe('');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('eq', () => {
|
|
60
|
+
it('returns true for equal states', () => {
|
|
61
|
+
const a = [{ id: 'col', desc: false }];
|
|
62
|
+
const b = [{ id: 'col', desc: false }];
|
|
63
|
+
expect(parseAsSortingState.eq(a, b)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('returns true for equal multi-column states', () => {
|
|
66
|
+
const a = [
|
|
67
|
+
{ id: 'col1', desc: false },
|
|
68
|
+
{ id: 'col2', desc: true },
|
|
69
|
+
];
|
|
70
|
+
const b = [
|
|
71
|
+
{ id: 'col1', desc: false },
|
|
72
|
+
{ id: 'col2', desc: true },
|
|
73
|
+
];
|
|
74
|
+
expect(parseAsSortingState.eq(a, b)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
// The order of the sort columns matters for multi-column sorting.
|
|
77
|
+
it('returns false for different order in multi-column', () => {
|
|
78
|
+
const a = [
|
|
79
|
+
{ id: 'col1', desc: false },
|
|
80
|
+
{ id: 'col2', desc: true },
|
|
81
|
+
];
|
|
82
|
+
const b = [
|
|
83
|
+
{ id: 'col2', desc: true },
|
|
84
|
+
{ id: 'col1', desc: false },
|
|
85
|
+
];
|
|
86
|
+
expect(parseAsSortingState.eq(a, b)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it('returns false for different ids', () => {
|
|
89
|
+
const a = [{ id: 'col1', desc: false }];
|
|
90
|
+
const b = [{ id: 'col2', desc: false }];
|
|
91
|
+
expect(parseAsSortingState.eq(a, b)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it('returns false for different desc', () => {
|
|
94
|
+
const a = [{ id: 'col', desc: false }];
|
|
95
|
+
const b = [{ id: 'col', desc: true }];
|
|
96
|
+
expect(parseAsSortingState.eq(a, b)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
it('returns true for both empty', () => {
|
|
99
|
+
expect(parseAsSortingState.eq([], [])).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
it('returns false for one empty, one not', () => {
|
|
102
|
+
expect(parseAsSortingState.eq([], [{ id: 'col', desc: false }])).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('parseAsColumnVisibilityStateWithColumns', () => {
|
|
107
|
+
const allColumns = ['a', 'b', 'c'];
|
|
108
|
+
const parser = parseAsColumnVisibilityStateWithColumns(allColumns);
|
|
109
|
+
it('parses empty string as all columns visible', () => {
|
|
110
|
+
expect(parser.parse('')).toEqual({ a: true, b: true, c: true });
|
|
111
|
+
});
|
|
112
|
+
it('parses comma-separated columns as only those visible', () => {
|
|
113
|
+
expect(parser.parse('a,b')).toEqual({ a: true, b: true, c: false });
|
|
114
|
+
expect(parser.parse('b')).toEqual({ a: false, b: true, c: false });
|
|
115
|
+
});
|
|
116
|
+
it('serializes partial visibility as comma-separated columns', () => {
|
|
117
|
+
expect(parser.serialize({ a: true, b: false, c: true })).toBe('a,c');
|
|
118
|
+
expect(parser.serialize({ a: false, b: true, c: false })).toBe('b');
|
|
119
|
+
});
|
|
120
|
+
it('eq returns true for equal visibility', () => {
|
|
121
|
+
expect(parser.eq({ a: true, b: false, c: true }, { a: true, b: false, c: true })).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
it('eq returns false for different visibility', () => {
|
|
124
|
+
expect(parser.eq({ a: true, b: false, c: true }, { a: false, b: false, c: true })).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('parseAsColumnPinningState', () => {
|
|
128
|
+
const parser = parseAsColumnPinningState;
|
|
129
|
+
it('parses empty string as no pinned columns', () => {
|
|
130
|
+
expect(parser.parse('')).toEqual({ left: [], right: [] });
|
|
131
|
+
});
|
|
132
|
+
it('parses comma-separated columns as left-pinned', () => {
|
|
133
|
+
expect(parser.parse('a,b')).toEqual({ left: ['a', 'b'], right: [] });
|
|
134
|
+
expect(parser.parse('c')).toEqual({ left: ['c'], right: [] });
|
|
135
|
+
});
|
|
136
|
+
it('serializes left-pinned columns as comma-separated string', () => {
|
|
137
|
+
expect(parser.serialize({ left: ['a', 'b'], right: [] })).toBe('a,b');
|
|
138
|
+
expect(parser.serialize({ left: [], right: [] })).toBe('');
|
|
139
|
+
});
|
|
140
|
+
it('eq returns true for equal pinning', () => {
|
|
141
|
+
expect(parser.eq({ left: ['a', 'b'], right: [] }, { left: ['a', 'b'], right: [] })).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
it('eq returns false for different pinning', () => {
|
|
144
|
+
expect(parser.eq({ left: ['a', 'b'], right: [] }, { left: ['b', 'a'], right: [] })).toBe(false);
|
|
145
|
+
expect(parser.eq({ left: ['a'], right: [] }, { left: ['a', 'b'], right: [] })).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('parseAsNumericFilter', () => {
|
|
149
|
+
describe('parse', () => {
|
|
150
|
+
it('parses gte format', () => {
|
|
151
|
+
expect(parseAsNumericFilter.parse('gte_5')).toEqual({ filterValue: '>=5', emptyOnly: false });
|
|
152
|
+
});
|
|
153
|
+
it('parses lte format', () => {
|
|
154
|
+
expect(parseAsNumericFilter.parse('lte_10')).toEqual({
|
|
155
|
+
filterValue: '<=10',
|
|
156
|
+
emptyOnly: false,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
it('parses gt format', () => {
|
|
160
|
+
expect(parseAsNumericFilter.parse('gt_3')).toEqual({ filterValue: '>3', emptyOnly: false });
|
|
161
|
+
});
|
|
162
|
+
it('parses lt format', () => {
|
|
163
|
+
expect(parseAsNumericFilter.parse('lt_7')).toEqual({ filterValue: '<7', emptyOnly: false });
|
|
164
|
+
});
|
|
165
|
+
it('parses eq format', () => {
|
|
166
|
+
expect(parseAsNumericFilter.parse('eq_5')).toEqual({ filterValue: '=5', emptyOnly: false });
|
|
167
|
+
});
|
|
168
|
+
it('parses empty keyword', () => {
|
|
169
|
+
expect(parseAsNumericFilter.parse('empty')).toEqual({ filterValue: '', emptyOnly: true });
|
|
170
|
+
});
|
|
171
|
+
it('returns default for invalid format', () => {
|
|
172
|
+
expect(parseAsNumericFilter.parse('invalid')).toEqual({ filterValue: '', emptyOnly: false });
|
|
173
|
+
});
|
|
174
|
+
it('returns default for empty string', () => {
|
|
175
|
+
expect(parseAsNumericFilter.parse('')).toEqual({ filterValue: '', emptyOnly: false });
|
|
176
|
+
});
|
|
177
|
+
it('returns default for undefined', () => {
|
|
178
|
+
expect(parseAsNumericFilter.parse(undefined)).toEqual({
|
|
179
|
+
filterValue: '',
|
|
180
|
+
emptyOnly: false,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
it('parses decimal values', () => {
|
|
184
|
+
expect(parseAsNumericFilter.parse('gte_3.14')).toEqual({
|
|
185
|
+
filterValue: '>=3.14',
|
|
186
|
+
emptyOnly: false,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
it('parses negative values', () => {
|
|
190
|
+
expect(parseAsNumericFilter.parse('lt_-5')).toEqual({ filterValue: '<-5', emptyOnly: false });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('serialize', () => {
|
|
194
|
+
it('serializes >= format', () => {
|
|
195
|
+
expect(parseAsNumericFilter.serialize({ filterValue: '>=5', emptyOnly: false })).toBe('gte_5');
|
|
196
|
+
});
|
|
197
|
+
it('serializes <= format', () => {
|
|
198
|
+
expect(parseAsNumericFilter.serialize({ filterValue: '<=10', emptyOnly: false })).toBe('lte_10');
|
|
199
|
+
});
|
|
200
|
+
it('serializes > format', () => {
|
|
201
|
+
expect(parseAsNumericFilter.serialize({ filterValue: '>3', emptyOnly: false })).toBe('gt_3');
|
|
202
|
+
});
|
|
203
|
+
it('serializes < format', () => {
|
|
204
|
+
expect(parseAsNumericFilter.serialize({ filterValue: '<7', emptyOnly: false })).toBe('lt_7');
|
|
205
|
+
});
|
|
206
|
+
it('serializes = format', () => {
|
|
207
|
+
expect(parseAsNumericFilter.serialize({ filterValue: '=5', emptyOnly: false })).toBe('eq_5');
|
|
208
|
+
});
|
|
209
|
+
it('serializes emptyOnly as empty', () => {
|
|
210
|
+
expect(parseAsNumericFilter.serialize({ filterValue: '', emptyOnly: true })).toBe('empty');
|
|
211
|
+
});
|
|
212
|
+
it('serializes empty filterValue as empty', () => {
|
|
213
|
+
expect(parseAsNumericFilter.serialize({ filterValue: '', emptyOnly: false })).toBe('empty');
|
|
214
|
+
});
|
|
215
|
+
it('returns null for invalid filterValue', () => {
|
|
216
|
+
expect(parseAsNumericFilter.serialize({ filterValue: 'invalid', emptyOnly: false })).toBe(null);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe('eq', () => {
|
|
220
|
+
it('returns true for equal values', () => {
|
|
221
|
+
expect(parseAsNumericFilter.eq({ filterValue: '>=5', emptyOnly: false }, { filterValue: '>=5', emptyOnly: false })).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
it('returns false for different filterValue', () => {
|
|
224
|
+
expect(parseAsNumericFilter.eq({ filterValue: '>=5', emptyOnly: false }, { filterValue: '>=10', emptyOnly: false })).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
it('returns false for different emptyOnly', () => {
|
|
227
|
+
expect(parseAsNumericFilter.eq({ filterValue: '', emptyOnly: true }, { filterValue: '', emptyOnly: false })).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
//# sourceMappingURL=nuqs.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nuqs.test.js","sourceRoot":"","sources":["../../src/components/nuqs.test.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SortingState } from '@tanstack/table-core';\nimport { describe, expect, it } from 'vitest';\n\nimport {\n parseAsColumnPinningState,\n parseAsColumnVisibilityStateWithColumns,\n parseAsNumericFilter,\n parseAsSortingState,\n} from './nuqs.js';\n\ndescribe('parseAsSortingState', () => {\n describe('parse', () => {\n it('parses valid asc', () => {\n expect(parseAsSortingState.parse('col:asc')).toEqual([{ id: 'col', desc: false }]);\n });\n it('parses valid desc', () => {\n expect(parseAsSortingState.parse('col:desc')).toEqual([{ id: 'col', desc: true }]);\n });\n it('parses multiple columns', () => {\n expect(parseAsSortingState.parse('col1:asc,col2:desc')).toEqual([\n { id: 'col1', desc: false },\n { id: 'col2', desc: true },\n ]);\n });\n it('ignores invalid columns in multi-column', () => {\n expect(parseAsSortingState.parse('col1:asc,invalid,foo:bar,col2:desc')).toEqual([\n { id: 'col1', desc: false },\n { id: 'col2', desc: true },\n ]);\n });\n it('returns [] for empty string', () => {\n expect(parseAsSortingState.parse('')).toEqual([]);\n });\n it('returns [] for missing id', () => {\n expect(parseAsSortingState.parse(':asc')).toEqual([]);\n });\n it('returns [] for invalid direction', () => {\n expect(parseAsSortingState.parse('col:foo')).toEqual([]);\n });\n it('returns [] for undefined', () => {\n expect(parseAsSortingState.parse(undefined as any)).toEqual([]);\n });\n });\n\n describe('serialize', () => {\n it('serializes asc', () => {\n const state: SortingState = [{ id: 'col', desc: false }];\n expect(parseAsSortingState.serialize(state)).toBe('col:asc');\n });\n it('serializes desc', () => {\n const state: SortingState = [{ id: 'col', desc: true }];\n expect(parseAsSortingState.serialize(state)).toBe('col:desc');\n });\n it('serializes multiple columns', () => {\n const state: SortingState = [\n { id: 'col1', desc: false },\n { id: 'col2', desc: true },\n ];\n expect(parseAsSortingState.serialize(state)).toBe('col1:asc,col2:desc');\n });\n it('serializes empty array as null', () => {\n expect(parseAsSortingState.serialize([])).toBe(null);\n });\n it('serializes missing id as empty string', () => {\n expect(parseAsSortingState.serialize([{ id: '', desc: false }])).toBe('');\n });\n });\n\n describe('eq', () => {\n it('returns true for equal states', () => {\n const a: SortingState = [{ id: 'col', desc: false }];\n const b: SortingState = [{ id: 'col', desc: false }];\n expect(parseAsSortingState.eq(a, b)).toBe(true);\n });\n it('returns true for equal multi-column states', () => {\n const a: SortingState = [\n { id: 'col1', desc: false },\n { id: 'col2', desc: true },\n ];\n const b: SortingState = [\n { id: 'col1', desc: false },\n { id: 'col2', desc: true },\n ];\n expect(parseAsSortingState.eq(a, b)).toBe(true);\n });\n // The order of the sort columns matters for multi-column sorting.\n it('returns false for different order in multi-column', () => {\n const a: SortingState = [\n { id: 'col1', desc: false },\n { id: 'col2', desc: true },\n ];\n const b: SortingState = [\n { id: 'col2', desc: true },\n { id: 'col1', desc: false },\n ];\n expect(parseAsSortingState.eq(a, b)).toBe(false);\n });\n it('returns false for different ids', () => {\n const a: SortingState = [{ id: 'col1', desc: false }];\n const b: SortingState = [{ id: 'col2', desc: false }];\n expect(parseAsSortingState.eq(a, b)).toBe(false);\n });\n it('returns false for different desc', () => {\n const a: SortingState = [{ id: 'col', desc: false }];\n const b: SortingState = [{ id: 'col', desc: true }];\n expect(parseAsSortingState.eq(a, b)).toBe(false);\n });\n it('returns true for both empty', () => {\n expect(parseAsSortingState.eq([], [])).toBe(true);\n });\n it('returns false for one empty, one not', () => {\n expect(parseAsSortingState.eq([], [{ id: 'col', desc: false }])).toBe(false);\n });\n });\n});\n\ndescribe('parseAsColumnVisibilityStateWithColumns', () => {\n const allColumns = ['a', 'b', 'c'];\n const parser = parseAsColumnVisibilityStateWithColumns(allColumns);\n\n it('parses empty string as all columns visible', () => {\n expect(parser.parse('')).toEqual({ a: true, b: true, c: true });\n });\n\n it('parses comma-separated columns as only those visible', () => {\n expect(parser.parse('a,b')).toEqual({ a: true, b: true, c: false });\n expect(parser.parse('b')).toEqual({ a: false, b: true, c: false });\n });\n\n it('serializes partial visibility as comma-separated columns', () => {\n expect(parser.serialize({ a: true, b: false, c: true })).toBe('a,c');\n expect(parser.serialize({ a: false, b: true, c: false })).toBe('b');\n });\n\n it('eq returns true for equal visibility', () => {\n expect(parser.eq({ a: true, b: false, c: true }, { a: true, b: false, c: true })).toBe(true);\n });\n\n it('eq returns false for different visibility', () => {\n expect(parser.eq({ a: true, b: false, c: true }, { a: false, b: false, c: true })).toBe(false);\n });\n});\n\ndescribe('parseAsColumnPinningState', () => {\n const parser = parseAsColumnPinningState;\n\n it('parses empty string as no pinned columns', () => {\n expect(parser.parse('')).toEqual({ left: [], right: [] });\n });\n\n it('parses comma-separated columns as left-pinned', () => {\n expect(parser.parse('a,b')).toEqual({ left: ['a', 'b'], right: [] });\n expect(parser.parse('c')).toEqual({ left: ['c'], right: [] });\n });\n\n it('serializes left-pinned columns as comma-separated string', () => {\n expect(parser.serialize({ left: ['a', 'b'], right: [] })).toBe('a,b');\n expect(parser.serialize({ left: [], right: [] })).toBe('');\n });\n\n it('eq returns true for equal pinning', () => {\n expect(parser.eq({ left: ['a', 'b'], right: [] }, { left: ['a', 'b'], right: [] })).toBe(true);\n });\n\n it('eq returns false for different pinning', () => {\n expect(parser.eq({ left: ['a', 'b'], right: [] }, { left: ['b', 'a'], right: [] })).toBe(false);\n expect(parser.eq({ left: ['a'], right: [] }, { left: ['a', 'b'], right: [] })).toBe(false);\n });\n});\n\ndescribe('parseAsNumericFilter', () => {\n describe('parse', () => {\n it('parses gte format', () => {\n expect(parseAsNumericFilter.parse('gte_5')).toEqual({ filterValue: '>=5', emptyOnly: false });\n });\n it('parses lte format', () => {\n expect(parseAsNumericFilter.parse('lte_10')).toEqual({\n filterValue: '<=10',\n emptyOnly: false,\n });\n });\n it('parses gt format', () => {\n expect(parseAsNumericFilter.parse('gt_3')).toEqual({ filterValue: '>3', emptyOnly: false });\n });\n it('parses lt format', () => {\n expect(parseAsNumericFilter.parse('lt_7')).toEqual({ filterValue: '<7', emptyOnly: false });\n });\n it('parses eq format', () => {\n expect(parseAsNumericFilter.parse('eq_5')).toEqual({ filterValue: '=5', emptyOnly: false });\n });\n it('parses empty keyword', () => {\n expect(parseAsNumericFilter.parse('empty')).toEqual({ filterValue: '', emptyOnly: true });\n });\n it('returns default for invalid format', () => {\n expect(parseAsNumericFilter.parse('invalid')).toEqual({ filterValue: '', emptyOnly: false });\n });\n it('returns default for empty string', () => {\n expect(parseAsNumericFilter.parse('')).toEqual({ filterValue: '', emptyOnly: false });\n });\n it('returns default for undefined', () => {\n expect(parseAsNumericFilter.parse(undefined as any)).toEqual({\n filterValue: '',\n emptyOnly: false,\n });\n });\n it('parses decimal values', () => {\n expect(parseAsNumericFilter.parse('gte_3.14')).toEqual({\n filterValue: '>=3.14',\n emptyOnly: false,\n });\n });\n it('parses negative values', () => {\n expect(parseAsNumericFilter.parse('lt_-5')).toEqual({ filterValue: '<-5', emptyOnly: false });\n });\n });\n\n describe('serialize', () => {\n it('serializes >= format', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: '>=5', emptyOnly: false })).toBe(\n 'gte_5',\n );\n });\n it('serializes <= format', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: '<=10', emptyOnly: false })).toBe(\n 'lte_10',\n );\n });\n it('serializes > format', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: '>3', emptyOnly: false })).toBe('gt_3');\n });\n it('serializes < format', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: '<7', emptyOnly: false })).toBe('lt_7');\n });\n it('serializes = format', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: '=5', emptyOnly: false })).toBe('eq_5');\n });\n it('serializes emptyOnly as empty', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: '', emptyOnly: true })).toBe('empty');\n });\n it('serializes empty filterValue as empty', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: '', emptyOnly: false })).toBe('empty');\n });\n it('returns null for invalid filterValue', () => {\n expect(parseAsNumericFilter.serialize({ filterValue: 'invalid', emptyOnly: false })).toBe(\n null,\n );\n });\n });\n\n describe('eq', () => {\n it('returns true for equal values', () => {\n expect(\n parseAsNumericFilter.eq(\n { filterValue: '>=5', emptyOnly: false },\n { filterValue: '>=5', emptyOnly: false },\n ),\n ).toBe(true);\n });\n it('returns false for different filterValue', () => {\n expect(\n parseAsNumericFilter.eq(\n { filterValue: '>=5', emptyOnly: false },\n { filterValue: '>=10', emptyOnly: false },\n ),\n ).toBe(false);\n });\n it('returns false for different emptyOnly', () => {\n expect(\n parseAsNumericFilter.eq(\n { filterValue: '', emptyOnly: true },\n { filterValue: '', emptyOnly: false },\n ),\n ).toBe(false);\n });\n });\n});\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,5 +6,8 @@ export { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js
|
|
|
6
6
|
export { MultiSelectColumnFilter } from './components/MultiSelectColumnFilter.js';
|
|
7
7
|
export { NumericInputColumnFilter, parseNumericFilter, numericColumnFilterFn, type NumericColumnFilterValue, } from './components/NumericInputColumnFilter.js';
|
|
8
8
|
export { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';
|
|
9
|
+
export { useAutoSizeColumns } from './components/useAutoSizeColumns.js';
|
|
9
10
|
export { OverlayTrigger, type OverlayTriggerProps } from './components/OverlayTrigger.js';
|
|
11
|
+
export { PresetFilterDropdown } from './components/PresetFilterDropdown.js';
|
|
12
|
+
export { NuqsAdapter, parseAsSortingState, parseAsColumnVisibilityStateWithColumns, parseAsColumnPinningState, parseAsNumericFilter, } from './components/nuqs.js';
|
|
10
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,wBAAwB,GAC9B,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,wBAAwB,GAC9B,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,uCAAuC,EACvC,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,8 @@ export { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js
|
|
|
7
7
|
export { MultiSelectColumnFilter } from './components/MultiSelectColumnFilter.js';
|
|
8
8
|
export { NumericInputColumnFilter, parseNumericFilter, numericColumnFilterFn, } from './components/NumericInputColumnFilter.js';
|
|
9
9
|
export { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';
|
|
10
|
+
export { useAutoSizeColumns } from './components/useAutoSizeColumns.js';
|
|
10
11
|
export { OverlayTrigger } from './components/OverlayTrigger.js';
|
|
12
|
+
export { PresetFilterDropdown } from './components/PresetFilterDropdown.js';
|
|
13
|
+
export { NuqsAdapter, parseAsSortingState, parseAsColumnVisibilityStateWithColumns, parseAsColumnPinningState, parseAsNumericFilter, } from './components/nuqs.js';
|
|
11
14
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,GAEtB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,cAAc,EAA4B,MAAM,gCAAgC,CAAC","sourcesContent":["// Augment @tanstack/react-table types\nimport './react-table.js';\n\nexport {\n TanstackTable,\n TanstackTableCard,\n TanstackTableEmptyState,\n} from './components/TanstackTable.js';\nexport { ColumnManager } from './components/ColumnManager.js';\nexport { TanstackTableDownloadButton } from './components/TanstackTableDownloadButton.js';\nexport { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js';\nexport { MultiSelectColumnFilter } from './components/MultiSelectColumnFilter.js';\nexport {\n NumericInputColumnFilter,\n parseNumericFilter,\n numericColumnFilterFn,\n type NumericColumnFilterValue,\n} from './components/NumericInputColumnFilter.js';\nexport { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';\nexport { OverlayTrigger, type OverlayTriggerProps } from './components/OverlayTrigger.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,GAEtB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,cAAc,EAA4B,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,uCAAuC,EACvC,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC","sourcesContent":["// Augment @tanstack/react-table types\nimport './react-table.js';\n\nexport {\n TanstackTable,\n TanstackTableCard,\n TanstackTableEmptyState,\n} from './components/TanstackTable.js';\nexport { ColumnManager } from './components/ColumnManager.js';\nexport { TanstackTableDownloadButton } from './components/TanstackTableDownloadButton.js';\nexport { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js';\nexport { MultiSelectColumnFilter } from './components/MultiSelectColumnFilter.js';\nexport {\n NumericInputColumnFilter,\n parseNumericFilter,\n numericColumnFilterFn,\n type NumericColumnFilterValue,\n} from './components/NumericInputColumnFilter.js';\nexport { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';\nexport { useAutoSizeColumns } from './components/useAutoSizeColumns.js';\nexport { OverlayTrigger, type OverlayTriggerProps } from './components/OverlayTrigger.js';\nexport { PresetFilterDropdown } from './components/PresetFilterDropdown.js';\nexport {\n NuqsAdapter,\n parseAsSortingState,\n parseAsColumnVisibilityStateWithColumns,\n parseAsColumnPinningState,\n parseAsNumericFilter,\n} from './components/nuqs.js';\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
"@tanstack/react-virtual": "^3.13.12",
|
|
24
24
|
"@tanstack/table-core": "^8.21.3",
|
|
25
25
|
"clsx": "^2.1.1",
|
|
26
|
+
"nuqs": "^2.8.2",
|
|
26
27
|
"react-bootstrap": "3.0.0-beta.5"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
30
|
-
"@types/node": "^22.19.
|
|
31
|
+
"@types/node": "^22.19.1",
|
|
31
32
|
"typescript": "^5.9.3",
|
|
32
33
|
"typescript-cp": "^0.1.9",
|
|
33
|
-
"vitest": "^4.0.
|
|
34
|
+
"vitest": "^4.0.14"
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -32,7 +32,7 @@ export function CategoricalColumnFilter<TData, TValue>({
|
|
|
32
32
|
renderValueLabel = defaultRenderValueLabel,
|
|
33
33
|
}: {
|
|
34
34
|
column: Column<TData, TValue>;
|
|
35
|
-
allColumnValues: TValue[];
|
|
35
|
+
allColumnValues: TValue[] | readonly TValue[];
|
|
36
36
|
renderValueLabel?: (props: { value: TValue; isSelected: boolean }) => JSX.Element;
|
|
37
37
|
}) {
|
|
38
38
|
const [mode, setMode] = useState<'include' | 'exclude'>('include');
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ColumnFiltersState, Table } from '@tanstack/react-table';
|
|
2
|
+
import { useMemo } from 'preact/compat';
|
|
3
|
+
import { ButtonGroup, Dropdown } from 'react-bootstrap';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compares two filter values for deep equality using JSON serialization.
|
|
7
|
+
*/
|
|
8
|
+
function filtersEqual(a: unknown, b: unknown): boolean {
|
|
9
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts all unique column IDs referenced across all preset options.
|
|
14
|
+
*/
|
|
15
|
+
function getRelevantColumnIds(options: Record<string, ColumnFiltersState>): Set<string> {
|
|
16
|
+
const columnIds = new Set<string>();
|
|
17
|
+
for (const filters of Object.values(options)) {
|
|
18
|
+
for (const filter of filters) {
|
|
19
|
+
columnIds.add(filter.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return columnIds;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gets the current filter values for the relevant columns from the table.
|
|
27
|
+
*/
|
|
28
|
+
function getRelevantFilters<TData>(
|
|
29
|
+
table: Table<TData>,
|
|
30
|
+
relevantColumnIds: Set<string>,
|
|
31
|
+
): ColumnFiltersState {
|
|
32
|
+
const allFilters = table.getState().columnFilters;
|
|
33
|
+
return allFilters.filter((f) => relevantColumnIds.has(f.id));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks if the current filters match a preset's filters.
|
|
38
|
+
* Both must have the same column IDs with equal values.
|
|
39
|
+
*/
|
|
40
|
+
function filtersMatchPreset(current: ColumnFiltersState, preset: ColumnFiltersState): boolean {
|
|
41
|
+
// If lengths differ, they don't match
|
|
42
|
+
if (current.length !== preset.length) return false;
|
|
43
|
+
|
|
44
|
+
// For empty presets, current must also be empty
|
|
45
|
+
if (preset.length === 0) return current.length === 0;
|
|
46
|
+
|
|
47
|
+
// Check that every preset filter exists in current with the same value
|
|
48
|
+
for (const presetFilter of preset) {
|
|
49
|
+
const currentFilter = current.find((f) => f.id === presetFilter.id);
|
|
50
|
+
if (!currentFilter || !filtersEqual(currentFilter.value, presetFilter.value)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A dropdown component that allows users to select from preset filter configurations.
|
|
60
|
+
* The selected state is derived from the table's current column filters.
|
|
61
|
+
* If no preset matches, a "Custom" option is shown as selected.
|
|
62
|
+
*
|
|
63
|
+
* Currently, this component expects that the filters states are arrays.
|
|
64
|
+
*/
|
|
65
|
+
export function PresetFilterDropdown<OptionName extends string, TData>({
|
|
66
|
+
table,
|
|
67
|
+
options,
|
|
68
|
+
label = 'Filter',
|
|
69
|
+
onSelect,
|
|
70
|
+
}: {
|
|
71
|
+
/** The TanStack Table instance */
|
|
72
|
+
table: Table<TData>;
|
|
73
|
+
/** Mapping of option names to their filter configurations */
|
|
74
|
+
options: Record<OptionName, ColumnFiltersState>;
|
|
75
|
+
/** Label prefix for the dropdown button (e.g., "Filter") */
|
|
76
|
+
label?: string;
|
|
77
|
+
/** Callback when an option is selected, useful for side effects like column visibility */
|
|
78
|
+
onSelect?: (optionName: OptionName) => void;
|
|
79
|
+
}) {
|
|
80
|
+
const relevantColumnIds = getRelevantColumnIds(options);
|
|
81
|
+
|
|
82
|
+
const currentRelevantFilters = useMemo(
|
|
83
|
+
() => getRelevantFilters(table, relevantColumnIds),
|
|
84
|
+
[table, relevantColumnIds],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Find which option matches the current filters
|
|
88
|
+
const selectedOption = useMemo<OptionName | null>(() => {
|
|
89
|
+
for (const [optionName, presetFilters] of Object.entries(options)) {
|
|
90
|
+
if (filtersMatchPreset(currentRelevantFilters, presetFilters as ColumnFiltersState)) {
|
|
91
|
+
return optionName as OptionName;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null; // No preset matches - custom filter state
|
|
95
|
+
}, [options, currentRelevantFilters]);
|
|
96
|
+
|
|
97
|
+
const handleOptionClick = (optionName: OptionName) => {
|
|
98
|
+
const presetFilters = options[optionName];
|
|
99
|
+
|
|
100
|
+
// Get current filters, removing any that are in our relevant columns
|
|
101
|
+
const currentFilters = table.getState().columnFilters;
|
|
102
|
+
const preservedFilters = currentFilters.filter((f) => !relevantColumnIds.has(f.id));
|
|
103
|
+
|
|
104
|
+
// For columns not in the preset, explicitly set empty filter to clear them
|
|
105
|
+
// This ensures the table's onColumnFiltersChange handler can sync the cleared state
|
|
106
|
+
const clearedFilters = Array.from(relevantColumnIds)
|
|
107
|
+
.filter((colId) => !presetFilters.some((f) => f.id === colId))
|
|
108
|
+
.map((colId) => ({
|
|
109
|
+
id: colId,
|
|
110
|
+
// TODO: This expects that we are only clearing filters whose state is an array.
|
|
111
|
+
value: [],
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// Combine preserved filters with the new preset filters and cleared filters
|
|
115
|
+
const newFilters = [...preservedFilters, ...presetFilters, ...clearedFilters];
|
|
116
|
+
table.setColumnFilters(newFilters);
|
|
117
|
+
|
|
118
|
+
onSelect?.(optionName);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const displayLabel = selectedOption ?? 'Custom';
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Dropdown as={ButtonGroup}>
|
|
125
|
+
<Dropdown.Toggle variant="tanstack-table">
|
|
126
|
+
<i class="bi bi-funnel me-2" aria-hidden="true" />
|
|
127
|
+
{label}: {displayLabel}
|
|
128
|
+
</Dropdown.Toggle>
|
|
129
|
+
<Dropdown.Menu>
|
|
130
|
+
{Object.keys(options).map((optionName) => {
|
|
131
|
+
const isSelected = selectedOption === optionName;
|
|
132
|
+
return (
|
|
133
|
+
<Dropdown.Item
|
|
134
|
+
key={optionName}
|
|
135
|
+
as="button"
|
|
136
|
+
type="button"
|
|
137
|
+
active={isSelected}
|
|
138
|
+
onClick={() => handleOptionClick(optionName as OptionName)}
|
|
139
|
+
>
|
|
140
|
+
<i class={`bi ${isSelected ? 'bi-check-circle-fill' : 'bi-circle'} me-2`} />
|
|
141
|
+
{optionName}
|
|
142
|
+
</Dropdown.Item>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
{/* Show Custom option only when no preset matches */}
|
|
146
|
+
{selectedOption === null && (
|
|
147
|
+
<Dropdown.Item as="button" type="button" active disabled>
|
|
148
|
+
<i class="bi bi-check-circle-fill me-2" />
|
|
149
|
+
Custom
|
|
150
|
+
</Dropdown.Item>
|
|
151
|
+
)}
|
|
152
|
+
</Dropdown.Menu>
|
|
153
|
+
</Dropdown>
|
|
154
|
+
);
|
|
155
|
+
}
|