@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.
- package/CHANGELOG.md +16 -0
- package/README.md +61 -4
- package/dist/components/TanstackTable.d.ts +15 -13
- package/dist/components/TanstackTable.d.ts.map +1 -1
- package/dist/components/TanstackTable.js +22 -11
- package/dist/components/TanstackTable.js.map +1 -1
- package/dist/components/TanstackTableDownloadButton.d.ts +5 -3
- package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
- package/dist/components/TanstackTableDownloadButton.js +6 -5
- package/dist/components/TanstackTableDownloadButton.js.map +1 -1
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/components/TanstackTable.tsx +35 -21
- package/src/components/TanstackTableDownloadButton.tsx +41 -30
- package/src/components/nuqs.test.ts +276 -0
- package/src/components/nuqs.tsx +230 -0
- package/src/index.ts +7 -0
|
@@ -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.
|
|
476
|
-
* @param params.
|
|
477
|
-
* @param params.
|
|
478
|
-
* @param params.globalFilter
|
|
479
|
-
* @param params.globalFilter.
|
|
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
|
-
|
|
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
|
|
503
|
-
|
|
504
|
-
|
|
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={
|
|
570
|
+
value={inputValue}
|
|
563
571
|
autoComplete="off"
|
|
564
572
|
onInput={(e) => {
|
|
565
573
|
if (!(e.target instanceof HTMLInputElement)) return;
|
|
566
|
-
|
|
574
|
+
const value = e.target.value;
|
|
575
|
+
setInputValue(value);
|
|
576
|
+
debouncedSetFilter(value);
|
|
567
577
|
}}
|
|
568
578
|
/>
|
|
569
|
-
{
|
|
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={() =>
|
|
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={
|
|
584
|
-
{
|
|
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}
|
|
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}
|
|
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
|
+
});
|