@prairielearn/ui 1.5.0 → 1.7.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.
@@ -3,10 +3,11 @@ import { useVirtualizer } from '@tanstack/react-virtual';
3
3
  import type { Cell, Header, Row, Table } from '@tanstack/table-core';
4
4
  import clsx from 'clsx';
5
5
  import type { ComponentChildren } from 'preact';
6
- import { useEffect, useMemo, useRef } from 'preact/hooks';
6
+ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
7
7
  import type { JSX } from 'preact/jsx-runtime';
8
8
  import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
9
9
  import Tooltip from 'react-bootstrap/Tooltip';
10
+ import { useDebouncedCallback } from 'use-debounce';
10
11
 
11
12
  import type { ComponentProps } from '@prairielearn/preact-cjs';
12
13
  import { run } from '@prairielearn/run';
@@ -472,12 +473,11 @@ export function TanstackTable<RowDataModel>({
472
473
  * @param params.singularLabel - The singular label for a single row in the table, e.g. "student"
473
474
  * @param params.pluralLabel - The plural label for multiple rows in the table, e.g. "students"
474
475
  * @param params.headerButtons - The buttons to display in the header
475
- * @param params.columnManagerButtons - The buttons to display next to the column manager (View button)
476
- * @param params.columnManagerTopContent - Optional content to display at the top of the column manager (View) dropdown menu
477
- * @param params.globalFilter - State management for the global filter
478
- * @param params.globalFilter.value
479
- * @param params.globalFilter.setValue
480
- * @param params.globalFilter.placeholder
476
+ * @param params.columnManager - Optional configuration for the column manager. See {@link ColumnManager} for more details.
477
+ * @param params.columnManager.buttons - The buttons to display next to the column manager (View button)
478
+ * @param params.columnManager.topContent - Optional content to display at the top of the column manager (View) dropdown menu
479
+ * @param params.globalFilter - Configuration for the global filter
480
+ * @param params.globalFilter.placeholder - Placeholder text for the search input
481
481
  * @param params.tableOptions - Specific options for the table. See {@link TanstackTableProps} for more details.
482
482
  * @param params.downloadButtonOptions - Specific options for the download button. See {@link TanstackTableDownloadButtonProps} for more details.
483
483
  */
@@ -487,8 +487,7 @@ export function TanstackTableCard<RowDataModel>({
487
487
  singularLabel,
488
488
  pluralLabel,
489
489
  headerButtons,
490
- columnManagerButtons,
491
- columnManagerTopContent,
490
+ columnManager,
492
491
  globalFilter,
493
492
  tableOptions,
494
493
  downloadButtonOptions,
@@ -499,22 +498,31 @@ export function TanstackTableCard<RowDataModel>({
499
498
  title: string;
500
499
  singularLabel: string;
501
500
  pluralLabel: string;
502
- headerButtons: JSX.Element;
503
- columnManagerButtons?: JSX.Element;
504
- columnManagerTopContent?: JSX.Element;
501
+ headerButtons?: JSX.Element;
502
+ columnManager?: {
503
+ buttons?: JSX.Element;
504
+ topContent?: JSX.Element;
505
+ };
505
506
  globalFilter: {
506
- value: string;
507
- setValue: (value: string) => void;
508
507
  placeholder: string;
509
508
  };
510
509
  tableOptions: Partial<Omit<TanstackTableProps<RowDataModel>, 'table'>>;
511
510
  downloadButtonOptions?: Omit<
512
511
  TanstackTableDownloadButtonProps<RowDataModel>,
513
512
  'table' | 'singularLabel' | 'pluralLabel'
514
- >;
513
+ > & { pluralLabel?: string; singularLabel?: string };
515
514
  } & Omit<ComponentProps<'div'>, 'class'>) {
516
515
  const searchInputRef = useRef<HTMLInputElement>(null);
517
516
 
517
+ const [inputValue, setInputValue] = useState(
518
+ () => (table.getState().globalFilter as string) ?? '',
519
+ );
520
+
521
+ // Debounce the filter update
522
+ const debouncedSetFilter = useDebouncedCallback((value: string) => {
523
+ table.setGlobalFilter(value);
524
+ }, 150);
525
+
518
526
  // Focus the search input when Ctrl+F is pressed
519
527
  useEffect(() => {
520
528
  function onKeyDown(event: KeyboardEvent) {
@@ -559,20 +567,26 @@ export function TanstackTableCard<RowDataModel>({
559
567
  class="form-control pl-ui-tanstack-table-search-input pl-ui-tanstack-table-focusable-shadow"
560
568
  aria-label={globalFilter.placeholder}
561
569
  placeholder={globalFilter.placeholder}
562
- value={globalFilter.value}
570
+ value={inputValue}
563
571
  autoComplete="off"
564
572
  onInput={(e) => {
565
573
  if (!(e.target instanceof HTMLInputElement)) return;
566
- globalFilter.setValue(e.target.value);
574
+ const value = e.target.value;
575
+ setInputValue(value);
576
+ debouncedSetFilter(value);
567
577
  }}
568
578
  />
569
- {globalFilter.value && (
579
+ {inputValue && (
570
580
  <OverlayTrigger overlay={<Tooltip>Clear search</Tooltip>}>
571
581
  <button
572
582
  type="button"
573
583
  class="btn btn-floating-icon"
574
584
  aria-label="Clear search"
575
- onClick={() => globalFilter.setValue('')}
585
+ onClick={() => {
586
+ setInputValue('');
587
+ debouncedSetFilter.cancel();
588
+ table.setGlobalFilter('');
589
+ }}
576
590
  >
577
591
  <i class="bi bi-x-circle-fill" aria-hidden="true" />
578
592
  </button>
@@ -580,8 +594,8 @@ export function TanstackTableCard<RowDataModel>({
580
594
  )}
581
595
  </div>
582
596
  <div class="d-flex flex-wrap flex-row align-items-center gap-2">
583
- <ColumnManager table={table} topContent={columnManagerTopContent} />
584
- {columnManagerButtons}
597
+ <ColumnManager table={table} topContent={columnManager?.topContent} />
598
+ {columnManager?.buttons}
585
599
  </div>
586
600
  <div class="ms-auto text-muted text-nowrap">
587
601
  Showing {displayedCount} of {totalCount} {totalCount === 1 ? singularLabel : pluralLabel}
@@ -8,17 +8,19 @@ export interface TanstackTableDownloadButtonProps<RowDataModel> {
8
8
  mapRowToData: (row: RowDataModel) => Record<string, string | number | null> | null;
9
9
  singularLabel: string;
10
10
  pluralLabel: string;
11
+ hasSelection: boolean;
11
12
  }
12
13
  /**
13
14
  * @param params
14
- * @param params.singularLabel - The singular label for a single row in the table, e.g. "student"
15
- * @param params.pluralLabel - The plural label for multiple rows in the table, e.g. "students"
16
15
  * @param params.table - The table model
17
16
  * @param params.filenameBase - The base filename for the downloads
18
17
  * @param params.mapRowToData - A function that maps a row to a record where the
19
18
  * keys are the column names, and the values are the cell values. The key order is important,
20
19
  * and should match the expected order of the columns in the CSV file. If the function returns null,
21
20
  * the row will be skipped.
21
+ * @param params.singularLabel - The singular label for a single row in the table, e.g. "student"
22
+ * @param params.pluralLabel - The plural label for multiple rows in the table, e.g. "students"
23
+ * @param params.hasSelection - Whether the table has selection enabled
22
24
  */
23
25
  export function TanstackTableDownloadButton<RowDataModel>({
24
26
  table,
@@ -26,6 +28,7 @@ export function TanstackTableDownloadButton<RowDataModel>({
26
28
  mapRowToData,
27
29
  singularLabel,
28
30
  pluralLabel,
31
+ hasSelection,
29
32
  }: TanstackTableDownloadButtonProps<RowDataModel>) {
30
33
  const allRows = table.getCoreRowModel().rows.map((row) => row.original);
31
34
  const allRowsJSON = allRows.map(mapRowToData).filter((row) => row !== null);
@@ -70,7 +73,7 @@ export function TanstackTableDownloadButton<RowDataModel>({
70
73
  disabled={allRowsJSON.length === 0}
71
74
  onClick={() => downloadJSONAsCSV(allRowsJSON, `${filenameBase}.csv`)}
72
75
  >
73
- All {pluralLabel} as CSV
76
+ All {pluralLabel} ({allRowsJSON.length}) as CSV
74
77
  </button>
75
78
  </li>
76
79
  <li role="presentation">
@@ -82,33 +85,39 @@ export function TanstackTableDownloadButton<RowDataModel>({
82
85
  disabled={allRowsJSON.length === 0}
83
86
  onClick={() => downloadAsJSON(allRowsJSON, `${filenameBase}.json`)}
84
87
  >
85
- All {pluralLabel} as JSON
86
- </button>
87
- </li>
88
- <li role="presentation">
89
- <button
90
- class="dropdown-item"
91
- type="button"
92
- role="menuitem"
93
- aria-label={`Download selected ${pluralLabel} as CSV file`}
94
- disabled={selectedRowsJSON.length === 0}
95
- onClick={() => downloadJSONAsCSV(selectedRowsJSON, `${filenameBase}_selected.csv`)}
96
- >
97
- Selected {selectedRowsJSON.length === 1 ? singularLabel : pluralLabel} as CSV
98
- </button>
99
- </li>
100
- <li role="presentation">
101
- <button
102
- class="dropdown-item"
103
- type="button"
104
- role="menuitem"
105
- aria-label={`Download selected ${pluralLabel} as JSON file`}
106
- disabled={selectedRowsJSON.length === 0}
107
- onClick={() => downloadAsJSON(selectedRowsJSON, `${filenameBase}_selected.json`)}
108
- >
109
- Selected {selectedRowsJSON.length === 1 ? singularLabel : pluralLabel} as JSON
88
+ All {pluralLabel} ({allRowsJSON.length}) as JSON
110
89
  </button>
111
90
  </li>
91
+ {hasSelection && (
92
+ <>
93
+ <li role="presentation">
94
+ <button
95
+ class="dropdown-item"
96
+ type="button"
97
+ role="menuitem"
98
+ aria-label={`Download selected ${pluralLabel} as CSV file`}
99
+ disabled={selectedRowsJSON.length === 0}
100
+ onClick={() => downloadJSONAsCSV(selectedRowsJSON, `${filenameBase}_selected.csv`)}
101
+ >
102
+ Selected {selectedRowsJSON.length === 1 ? singularLabel : pluralLabel} (
103
+ {selectedRowsJSON.length}) as CSV
104
+ </button>
105
+ </li>
106
+ <li role="presentation">
107
+ <button
108
+ class="dropdown-item"
109
+ type="button"
110
+ role="menuitem"
111
+ aria-label={`Download selected ${pluralLabel} as JSON file`}
112
+ disabled={selectedRowsJSON.length === 0}
113
+ onClick={() => downloadAsJSON(selectedRowsJSON, `${filenameBase}_selected.json`)}
114
+ >
115
+ Selected {selectedRowsJSON.length === 1 ? singularLabel : pluralLabel} (
116
+ {selectedRowsJSON.length}) as JSON
117
+ </button>
118
+ </li>
119
+ </>
120
+ )}
112
121
  <li role="presentation">
113
122
  <button
114
123
  class="dropdown-item"
@@ -118,7 +127,8 @@ export function TanstackTableDownloadButton<RowDataModel>({
118
127
  disabled={filteredRowsJSON.length === 0}
119
128
  onClick={() => downloadJSONAsCSV(filteredRowsJSON, `${filenameBase}_filtered.csv`)}
120
129
  >
121
- Filtered {filteredRowsJSON.length === 1 ? singularLabel : pluralLabel} as CSV
130
+ Filtered {filteredRowsJSON.length === 1 ? singularLabel : pluralLabel} (
131
+ {filteredRowsJSON.length}) as CSV
122
132
  </button>
123
133
  </li>
124
134
  <li role="presentation">
@@ -130,7 +140,8 @@ export function TanstackTableDownloadButton<RowDataModel>({
130
140
  disabled={filteredRowsJSON.length === 0}
131
141
  onClick={() => downloadAsJSON(filteredRowsJSON, `${filenameBase}_filtered.json`)}
132
142
  >
133
- Filtered {filteredRowsJSON.length === 1 ? singularLabel : pluralLabel} as JSON
143
+ Filtered {filteredRowsJSON.length === 1 ? singularLabel : pluralLabel} (
144
+ {filteredRowsJSON.length}) as JSON
134
145
  </button>
135
146
  </li>
136
147
  </ul>
@@ -0,0 +1,276 @@
1
+ import type { SortingState } from '@tanstack/table-core';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import {
5
+ parseAsColumnPinningState,
6
+ parseAsColumnVisibilityStateWithColumns,
7
+ parseAsNumericFilter,
8
+ parseAsSortingState,
9
+ } from './nuqs.js';
10
+
11
+ describe('parseAsSortingState', () => {
12
+ describe('parse', () => {
13
+ it('parses valid asc', () => {
14
+ expect(parseAsSortingState.parse('col:asc')).toEqual([{ id: 'col', desc: false }]);
15
+ });
16
+ it('parses valid desc', () => {
17
+ expect(parseAsSortingState.parse('col:desc')).toEqual([{ id: 'col', desc: true }]);
18
+ });
19
+ it('parses multiple columns', () => {
20
+ expect(parseAsSortingState.parse('col1:asc,col2:desc')).toEqual([
21
+ { id: 'col1', desc: false },
22
+ { id: 'col2', desc: true },
23
+ ]);
24
+ });
25
+ it('ignores invalid columns in multi-column', () => {
26
+ expect(parseAsSortingState.parse('col1:asc,invalid,foo:bar,col2:desc')).toEqual([
27
+ { id: 'col1', desc: false },
28
+ { id: 'col2', desc: true },
29
+ ]);
30
+ });
31
+ it('returns [] for empty string', () => {
32
+ expect(parseAsSortingState.parse('')).toEqual([]);
33
+ });
34
+ it('returns [] for missing id', () => {
35
+ expect(parseAsSortingState.parse(':asc')).toEqual([]);
36
+ });
37
+ it('returns [] for invalid direction', () => {
38
+ expect(parseAsSortingState.parse('col:foo')).toEqual([]);
39
+ });
40
+ it('returns [] for undefined', () => {
41
+ expect(parseAsSortingState.parse(undefined as any)).toEqual([]);
42
+ });
43
+ });
44
+
45
+ describe('serialize', () => {
46
+ it('serializes asc', () => {
47
+ const state: SortingState = [{ id: 'col', desc: false }];
48
+ expect(parseAsSortingState.serialize(state)).toBe('col:asc');
49
+ });
50
+ it('serializes desc', () => {
51
+ const state: SortingState = [{ id: 'col', desc: true }];
52
+ expect(parseAsSortingState.serialize(state)).toBe('col:desc');
53
+ });
54
+ it('serializes multiple columns', () => {
55
+ const state: SortingState = [
56
+ { id: 'col1', desc: false },
57
+ { id: 'col2', desc: true },
58
+ ];
59
+ expect(parseAsSortingState.serialize(state)).toBe('col1:asc,col2:desc');
60
+ });
61
+ it('serializes empty array as null', () => {
62
+ expect(parseAsSortingState.serialize([])).toBe(null);
63
+ });
64
+ it('serializes missing id as empty string', () => {
65
+ expect(parseAsSortingState.serialize([{ id: '', desc: false }])).toBe('');
66
+ });
67
+ });
68
+
69
+ describe('eq', () => {
70
+ it('returns true for equal states', () => {
71
+ const a: SortingState = [{ id: 'col', desc: false }];
72
+ const b: SortingState = [{ id: 'col', desc: false }];
73
+ expect(parseAsSortingState.eq(a, b)).toBe(true);
74
+ });
75
+ it('returns true for equal multi-column states', () => {
76
+ const a: SortingState = [
77
+ { id: 'col1', desc: false },
78
+ { id: 'col2', desc: true },
79
+ ];
80
+ const b: SortingState = [
81
+ { id: 'col1', desc: false },
82
+ { id: 'col2', desc: true },
83
+ ];
84
+ expect(parseAsSortingState.eq(a, b)).toBe(true);
85
+ });
86
+ // The order of the sort columns matters for multi-column sorting.
87
+ it('returns false for different order in multi-column', () => {
88
+ const a: SortingState = [
89
+ { id: 'col1', desc: false },
90
+ { id: 'col2', desc: true },
91
+ ];
92
+ const b: SortingState = [
93
+ { id: 'col2', desc: true },
94
+ { id: 'col1', desc: false },
95
+ ];
96
+ expect(parseAsSortingState.eq(a, b)).toBe(false);
97
+ });
98
+ it('returns false for different ids', () => {
99
+ const a: SortingState = [{ id: 'col1', desc: false }];
100
+ const b: SortingState = [{ id: 'col2', desc: false }];
101
+ expect(parseAsSortingState.eq(a, b)).toBe(false);
102
+ });
103
+ it('returns false for different desc', () => {
104
+ const a: SortingState = [{ id: 'col', desc: false }];
105
+ const b: SortingState = [{ id: 'col', desc: true }];
106
+ expect(parseAsSortingState.eq(a, b)).toBe(false);
107
+ });
108
+ it('returns true for both empty', () => {
109
+ expect(parseAsSortingState.eq([], [])).toBe(true);
110
+ });
111
+ it('returns false for one empty, one not', () => {
112
+ expect(parseAsSortingState.eq([], [{ id: 'col', desc: false }])).toBe(false);
113
+ });
114
+ });
115
+ });
116
+
117
+ describe('parseAsColumnVisibilityStateWithColumns', () => {
118
+ const allColumns = ['a', 'b', 'c'];
119
+ const parser = parseAsColumnVisibilityStateWithColumns(allColumns);
120
+
121
+ it('parses empty string as all columns visible', () => {
122
+ expect(parser.parse('')).toEqual({ a: true, b: true, c: true });
123
+ });
124
+
125
+ it('parses comma-separated columns as only those visible', () => {
126
+ expect(parser.parse('a,b')).toEqual({ a: true, b: true, c: false });
127
+ expect(parser.parse('b')).toEqual({ a: false, b: true, c: false });
128
+ });
129
+
130
+ it('serializes partial visibility as comma-separated columns', () => {
131
+ expect(parser.serialize({ a: true, b: false, c: true })).toBe('a,c');
132
+ expect(parser.serialize({ a: false, b: true, c: false })).toBe('b');
133
+ });
134
+
135
+ it('eq returns true for equal visibility', () => {
136
+ expect(parser.eq({ a: true, b: false, c: true }, { a: true, b: false, c: true })).toBe(true);
137
+ });
138
+
139
+ it('eq returns false for different visibility', () => {
140
+ expect(parser.eq({ a: true, b: false, c: true }, { a: false, b: false, c: true })).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe('parseAsColumnPinningState', () => {
145
+ const parser = parseAsColumnPinningState;
146
+
147
+ it('parses empty string as no pinned columns', () => {
148
+ expect(parser.parse('')).toEqual({ left: [], right: [] });
149
+ });
150
+
151
+ it('parses comma-separated columns as left-pinned', () => {
152
+ expect(parser.parse('a,b')).toEqual({ left: ['a', 'b'], right: [] });
153
+ expect(parser.parse('c')).toEqual({ left: ['c'], right: [] });
154
+ });
155
+
156
+ it('serializes left-pinned columns as comma-separated string', () => {
157
+ expect(parser.serialize({ left: ['a', 'b'], right: [] })).toBe('a,b');
158
+ expect(parser.serialize({ left: [], right: [] })).toBe('');
159
+ });
160
+
161
+ it('eq returns true for equal pinning', () => {
162
+ expect(parser.eq({ left: ['a', 'b'], right: [] }, { left: ['a', 'b'], right: [] })).toBe(true);
163
+ });
164
+
165
+ it('eq returns false for different pinning', () => {
166
+ expect(parser.eq({ left: ['a', 'b'], right: [] }, { left: ['b', 'a'], right: [] })).toBe(false);
167
+ expect(parser.eq({ left: ['a'], right: [] }, { left: ['a', 'b'], right: [] })).toBe(false);
168
+ });
169
+ });
170
+
171
+ describe('parseAsNumericFilter', () => {
172
+ describe('parse', () => {
173
+ it('parses gte format', () => {
174
+ expect(parseAsNumericFilter.parse('gte_5')).toEqual({ filterValue: '>=5', emptyOnly: false });
175
+ });
176
+ it('parses lte format', () => {
177
+ expect(parseAsNumericFilter.parse('lte_10')).toEqual({
178
+ filterValue: '<=10',
179
+ emptyOnly: false,
180
+ });
181
+ });
182
+ it('parses gt format', () => {
183
+ expect(parseAsNumericFilter.parse('gt_3')).toEqual({ filterValue: '>3', emptyOnly: false });
184
+ });
185
+ it('parses lt format', () => {
186
+ expect(parseAsNumericFilter.parse('lt_7')).toEqual({ filterValue: '<7', emptyOnly: false });
187
+ });
188
+ it('parses eq format', () => {
189
+ expect(parseAsNumericFilter.parse('eq_5')).toEqual({ filterValue: '=5', emptyOnly: false });
190
+ });
191
+ it('parses empty keyword', () => {
192
+ expect(parseAsNumericFilter.parse('empty')).toEqual({ filterValue: '', emptyOnly: true });
193
+ });
194
+ it('returns default for invalid format', () => {
195
+ expect(parseAsNumericFilter.parse('invalid')).toEqual({ filterValue: '', emptyOnly: false });
196
+ });
197
+ it('returns default for empty string', () => {
198
+ expect(parseAsNumericFilter.parse('')).toEqual({ filterValue: '', emptyOnly: false });
199
+ });
200
+ it('returns default for undefined', () => {
201
+ expect(parseAsNumericFilter.parse(undefined as any)).toEqual({
202
+ filterValue: '',
203
+ emptyOnly: false,
204
+ });
205
+ });
206
+ it('parses decimal values', () => {
207
+ expect(parseAsNumericFilter.parse('gte_3.14')).toEqual({
208
+ filterValue: '>=3.14',
209
+ emptyOnly: false,
210
+ });
211
+ });
212
+ it('parses negative values', () => {
213
+ expect(parseAsNumericFilter.parse('lt_-5')).toEqual({ filterValue: '<-5', emptyOnly: false });
214
+ });
215
+ });
216
+
217
+ describe('serialize', () => {
218
+ it('serializes >= format', () => {
219
+ expect(parseAsNumericFilter.serialize({ filterValue: '>=5', emptyOnly: false })).toBe(
220
+ 'gte_5',
221
+ );
222
+ });
223
+ it('serializes <= format', () => {
224
+ expect(parseAsNumericFilter.serialize({ filterValue: '<=10', emptyOnly: false })).toBe(
225
+ 'lte_10',
226
+ );
227
+ });
228
+ it('serializes > format', () => {
229
+ expect(parseAsNumericFilter.serialize({ filterValue: '>3', emptyOnly: false })).toBe('gt_3');
230
+ });
231
+ it('serializes < format', () => {
232
+ expect(parseAsNumericFilter.serialize({ filterValue: '<7', emptyOnly: false })).toBe('lt_7');
233
+ });
234
+ it('serializes = format', () => {
235
+ expect(parseAsNumericFilter.serialize({ filterValue: '=5', emptyOnly: false })).toBe('eq_5');
236
+ });
237
+ it('serializes emptyOnly as empty', () => {
238
+ expect(parseAsNumericFilter.serialize({ filterValue: '', emptyOnly: true })).toBe('empty');
239
+ });
240
+ it('serializes empty filterValue as empty', () => {
241
+ expect(parseAsNumericFilter.serialize({ filterValue: '', emptyOnly: false })).toBe('empty');
242
+ });
243
+ it('returns null for invalid filterValue', () => {
244
+ expect(parseAsNumericFilter.serialize({ filterValue: 'invalid', emptyOnly: false })).toBe(
245
+ null,
246
+ );
247
+ });
248
+ });
249
+
250
+ describe('eq', () => {
251
+ it('returns true for equal values', () => {
252
+ expect(
253
+ parseAsNumericFilter.eq(
254
+ { filterValue: '>=5', emptyOnly: false },
255
+ { filterValue: '>=5', emptyOnly: false },
256
+ ),
257
+ ).toBe(true);
258
+ });
259
+ it('returns false for different filterValue', () => {
260
+ expect(
261
+ parseAsNumericFilter.eq(
262
+ { filterValue: '>=5', emptyOnly: false },
263
+ { filterValue: '>=10', emptyOnly: false },
264
+ ),
265
+ ).toBe(false);
266
+ });
267
+ it('returns false for different emptyOnly', () => {
268
+ expect(
269
+ parseAsNumericFilter.eq(
270
+ { filterValue: '', emptyOnly: true },
271
+ { filterValue: '', emptyOnly: false },
272
+ ),
273
+ ).toBe(false);
274
+ });
275
+ });
276
+ });