@coveord/plasma-mantine 55.3.3 → 55.4.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/.turbo/turbo-build.log +3 -3
- package/.turbo/turbo-test.log +48 -45
- package/dist/.tsbuildinfo +1 -1
- package/dist/cjs/components/date-range-picker/DateRangePickerInlineCalendar.d.ts.map +1 -1
- package/dist/cjs/components/date-range-picker/DateRangePickerInlineCalendar.js +21 -3
- package/dist/cjs/components/date-range-picker/DateRangePickerInlineCalendar.js.map +1 -1
- package/dist/cjs/components/table/use-table.d.ts +10 -4
- package/dist/cjs/components/table/use-table.d.ts.map +1 -1
- package/dist/cjs/components/table/use-table.js +184 -12
- package/dist/cjs/components/table/use-table.js.map +1 -1
- package/dist/cjs/components/table/use-url-synced-state.d.ts +29 -0
- package/dist/cjs/components/table/use-url-synced-state.d.ts.map +1 -0
- package/dist/cjs/components/table/use-url-synced-state.js +58 -0
- package/dist/cjs/components/table/use-url-synced-state.js.map +1 -0
- package/dist/esm/components/date-range-picker/DateRangePickerInlineCalendar.d.ts.map +1 -1
- package/dist/esm/components/date-range-picker/DateRangePickerInlineCalendar.js +15 -2
- package/dist/esm/components/date-range-picker/DateRangePickerInlineCalendar.js.map +1 -1
- package/dist/esm/components/table/use-table.d.ts +10 -4
- package/dist/esm/components/table/use-table.d.ts.map +1 -1
- package/dist/esm/components/table/use-table.js +127 -8
- package/dist/esm/components/table/use-table.js.map +1 -1
- package/dist/esm/components/table/use-url-synced-state.d.ts +29 -0
- package/dist/esm/components/table/use-url-synced-state.d.ts.map +1 -0
- package/dist/esm/components/table/use-url-synced-state.js +41 -0
- package/dist/esm/components/table/use-url-synced-state.js.map +1 -0
- package/package.json +8 -8
- package/src/components/date-range-picker/DateRangePickerInlineCalendar.tsx +15 -1
- package/src/components/date-range-picker/__tests__/DateRangePickerInlineCalendar.spec.tsx +6 -4
- package/src/components/table/__tests__/TableColumnsSelector.spec.tsx +51 -0
- package/src/components/table/__tests__/TableDateRangePicker.spec.tsx +53 -1
- package/src/components/table/__tests__/TableFilter.spec.tsx +43 -0
- package/src/components/table/__tests__/TablePagination.spec.tsx +54 -0
- package/src/components/table/__tests__/TablePerPage.spec.tsx +55 -0
- package/src/components/table/__tests__/TablePredicate.spec.tsx +69 -0
- package/src/components/table/__tests__/Th.spec.tsx +35 -0
- package/src/components/table/__tests__/use-url-synced-state.unit.spec.ts +113 -0
- package/src/components/table/use-table.ts +120 -15
- package/src/components/table/use-url-synced-state.ts +70 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {ColumnDef, createColumnHelper} from '@tanstack/table-core';
|
|
2
|
-
import {render, screen} from '@test-utils';
|
|
2
|
+
import {render, screen, userEvent} from '@test-utils';
|
|
3
3
|
|
|
4
4
|
import {Table} from '../Table';
|
|
5
5
|
import {useTable} from '../use-table';
|
|
@@ -25,4 +25,56 @@ describe('Table.DateRangePicker', () => {
|
|
|
25
25
|
|
|
26
26
|
expect(screen.getByRole('button', {name: /jan 01, 2022 - jan 07, 2022/i})).toBeVisible();
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
describe('when url sync is activated', () => {
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
window.history.replaceState(null, '', '/');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('sets the selected date range in the url', async () => {
|
|
35
|
+
const user = userEvent.setup();
|
|
36
|
+
const Fixture = () => {
|
|
37
|
+
const store = useTable<RowData>({
|
|
38
|
+
initialState: {dateRange: [new Date(2022, 0, 1), new Date(2022, 0, 7)]},
|
|
39
|
+
syncWithUrl: true,
|
|
40
|
+
});
|
|
41
|
+
return (
|
|
42
|
+
<Table store={store} data={[{name: 'fruit'}, {name: 'vegetable'}]} columns={columns}>
|
|
43
|
+
<Table.Header>
|
|
44
|
+
<Table.DateRangePicker />
|
|
45
|
+
</Table.Header>
|
|
46
|
+
</Table>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
render(<Fixture />);
|
|
50
|
+
await user.click(screen.getByRole('button', {name: /jan 01, 2022 - jan 07, 2022/i}));
|
|
51
|
+
await screen.findByRole('dialog');
|
|
52
|
+
await user.clear(screen.getByRole('textbox', {name: /start/i}));
|
|
53
|
+
await user.type(screen.getByRole('textbox', {name: /start/i}), '2022-01-02');
|
|
54
|
+
await user.clear(screen.getByRole('textbox', {name: /end/i}));
|
|
55
|
+
await user.type(screen.getByRole('textbox', {name: /end/i}), '2022-01-08');
|
|
56
|
+
await user.click(screen.getByRole('button', {name: /apply/i}));
|
|
57
|
+
expect(window.location.search).toBe('?from=2022-01-02T00%3A00%3A00.000Z&to=2022-01-08T23%3A59%3A59.999Z');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('initially selects the specified date range from in the url', async () => {
|
|
61
|
+
window.history.replaceState(null, '', '?from=2022-01-02T00%3A00%3A00.000Z&to=2022-01-08T23%3A59%3A59.999Z');
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
const Fixture = () => {
|
|
64
|
+
const store = useTable<RowData>({
|
|
65
|
+
initialState: {dateRange: [new Date(2022, 0, 1), new Date(2022, 0, 7)]},
|
|
66
|
+
syncWithUrl: true,
|
|
67
|
+
});
|
|
68
|
+
return (
|
|
69
|
+
<Table store={store} data={[{name: 'fruit'}, {name: 'vegetable'}]} columns={columns}>
|
|
70
|
+
<Table.Header>
|
|
71
|
+
<Table.DateRangePicker />
|
|
72
|
+
</Table.Header>
|
|
73
|
+
</Table>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
render(<Fixture />);
|
|
77
|
+
expect(screen.getByRole('button', {name: /jan 02, 2022 - jan 08, 2022/i})).toBeVisible();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
28
80
|
});
|
|
@@ -102,4 +102,47 @@ describe('Table.Filter', () => {
|
|
|
102
102
|
expect(screen.getByRole('button', {name: /1 selected/i})).toBeInTheDocument();
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
describe('when url sync is activated', () => {
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
window.history.replaceState(null, '', '/');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('sets the current filter value in the url using the parameter "filter"', async () => {
|
|
112
|
+
const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
|
|
113
|
+
const Fixture = () => {
|
|
114
|
+
const store = useTable<RowData>({initialState: {globalFilter: ''}, syncWithUrl: true});
|
|
115
|
+
return (
|
|
116
|
+
<Table store={store} data={[{name: 'fruit'}, {name: 'vegetable'}]} columns={columns}>
|
|
117
|
+
<Table.Header>
|
|
118
|
+
<Table.Filter />
|
|
119
|
+
</Table.Header>
|
|
120
|
+
</Table>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
render(<Fixture />);
|
|
124
|
+
await user.type(screen.getByRole('textbox'), 'veg');
|
|
125
|
+
act(() => {
|
|
126
|
+
// 300 ms debounce on TableFilter input
|
|
127
|
+
vi.advanceTimersByTime(300);
|
|
128
|
+
});
|
|
129
|
+
expect(window.location.search).toBe('?filter=veg');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('determines the initial filter value from the url', async () => {
|
|
133
|
+
window.history.replaceState(null, '', '?filter=veg');
|
|
134
|
+
const Fixture = () => {
|
|
135
|
+
const store = useTable<RowData>({initialState: {globalFilter: ''}, syncWithUrl: true});
|
|
136
|
+
return (
|
|
137
|
+
<Table store={store} data={[{name: 'fruit'}, {name: 'vegetable'}]} columns={columns}>
|
|
138
|
+
<Table.Header>
|
|
139
|
+
<Table.Filter />
|
|
140
|
+
</Table.Header>
|
|
141
|
+
</Table>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
render(<Fixture />);
|
|
145
|
+
expect(screen.getByRole('textbox')).toHaveValue('veg');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
105
148
|
});
|
|
@@ -217,4 +217,58 @@ describe('Table.Pagination', () => {
|
|
|
217
217
|
expect(buttons).toHaveLength(1);
|
|
218
218
|
expect(buttons[0]).toHaveAccessibleName('change total pages');
|
|
219
219
|
});
|
|
220
|
+
|
|
221
|
+
describe('when url sync is activated', () => {
|
|
222
|
+
afterEach(() => {
|
|
223
|
+
window.history.replaceState(null, '', '/');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('sets the current page in the url using the parameter "page" counting from 1', async () => {
|
|
227
|
+
const data = [{name: 'fruit'}, {name: 'vegetable'}];
|
|
228
|
+
const user = userEvent.setup();
|
|
229
|
+
const Fixture = () => {
|
|
230
|
+
const store = useTable<RowData>({
|
|
231
|
+
initialState: {
|
|
232
|
+
pagination: {pageSize: 1, pageIndex: 0},
|
|
233
|
+
totalEntries: 3,
|
|
234
|
+
},
|
|
235
|
+
syncWithUrl: true,
|
|
236
|
+
});
|
|
237
|
+
return (
|
|
238
|
+
<Table store={store} data={data} columns={columns}>
|
|
239
|
+
<Table.Footer>
|
|
240
|
+
<Table.Pagination />
|
|
241
|
+
</Table.Footer>
|
|
242
|
+
</Table>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
render(<Fixture />);
|
|
246
|
+
await user.click(screen.queryByRole('button', {name: '2'}));
|
|
247
|
+
expect(window.location.search).toBe('?page=2');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('determines the initial page index from the url', async () => {
|
|
251
|
+
window.history.replaceState(null, '', '?page=2');
|
|
252
|
+
const data = [{name: 'fruit'}, {name: 'vegetable'}];
|
|
253
|
+
const Fixture = () => {
|
|
254
|
+
const store = useTable<RowData>({
|
|
255
|
+
initialState: {
|
|
256
|
+
pagination: {pageSize: 1, pageIndex: 0},
|
|
257
|
+
totalEntries: 3,
|
|
258
|
+
},
|
|
259
|
+
syncWithUrl: true,
|
|
260
|
+
});
|
|
261
|
+
return (
|
|
262
|
+
<Table store={store} data={data} columns={columns}>
|
|
263
|
+
<Table.Footer>
|
|
264
|
+
<Table.Pagination />
|
|
265
|
+
<Table.PerPage />
|
|
266
|
+
</Table.Footer>
|
|
267
|
+
</Table>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
render(<Fixture />);
|
|
271
|
+
expect(screen.getByRole('button', {name: '2', current: 'page'})).toBeVisible();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
220
274
|
});
|
|
@@ -122,4 +122,59 @@ describe('Table.PerPage', () => {
|
|
|
122
122
|
render(<Fixture />);
|
|
123
123
|
expect(screen.getByTestId('table-footer')).toBeEmptyDOMElement();
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
describe('when url sync is activated', () => {
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
window.history.replaceState(null, '', '/');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('sets the current page size in the url using the parameter "pageSize"', async () => {
|
|
132
|
+
const data = [{name: 'fruit'}, {name: 'vegetable'}];
|
|
133
|
+
const user = userEvent.setup();
|
|
134
|
+
const Fixture = () => {
|
|
135
|
+
const store = useTable<RowData>({
|
|
136
|
+
initialState: {
|
|
137
|
+
pagination: {pageIndex: 0, pageSize: 50},
|
|
138
|
+
totalEntries: 52,
|
|
139
|
+
},
|
|
140
|
+
syncWithUrl: true,
|
|
141
|
+
});
|
|
142
|
+
return (
|
|
143
|
+
<Table store={store} data={data} columns={columns}>
|
|
144
|
+
<Table.Footer>
|
|
145
|
+
<Table.Pagination />
|
|
146
|
+
<Table.PerPage />
|
|
147
|
+
</Table.Footer>
|
|
148
|
+
</Table>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
render(<Fixture />);
|
|
152
|
+
await user.click(screen.getByRole('radio', {name: '100'}));
|
|
153
|
+
expect(window.location.search).toBe('?pageSize=100');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('determines the initial pageSize from the url', async () => {
|
|
157
|
+
window.history.replaceState(null, '', '?pageSize=100');
|
|
158
|
+
const data = [{name: 'fruit'}, {name: 'vegetable'}];
|
|
159
|
+
const Fixture = () => {
|
|
160
|
+
const store = useTable<RowData>({
|
|
161
|
+
initialState: {
|
|
162
|
+
pagination: {pageIndex: 0, pageSize: 50},
|
|
163
|
+
totalEntries: 52,
|
|
164
|
+
},
|
|
165
|
+
syncWithUrl: true,
|
|
166
|
+
});
|
|
167
|
+
return (
|
|
168
|
+
<Table store={store} data={data} columns={columns}>
|
|
169
|
+
<Table.Footer>
|
|
170
|
+
<Table.Pagination />
|
|
171
|
+
<Table.PerPage />
|
|
172
|
+
</Table.Footer>
|
|
173
|
+
</Table>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
render(<Fixture />);
|
|
177
|
+
expect(screen.getByRole('radio', {name: '100'})).toBeChecked();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
125
180
|
});
|
|
@@ -11,6 +11,11 @@ const columns: Array<ColumnDef<RowData>> = [columnHelper.accessor('name', {enabl
|
|
|
11
11
|
|
|
12
12
|
describe('Table.Predicate', () => {
|
|
13
13
|
it('goes back to the first page when changing the predicate', async () => {
|
|
14
|
+
if (!HTMLElement.prototype.scrollIntoView) {
|
|
15
|
+
HTMLElement.prototype.scrollIntoView = () => {
|
|
16
|
+
vi.fn();
|
|
17
|
+
};
|
|
18
|
+
}
|
|
14
19
|
const user = userEvent.setup();
|
|
15
20
|
const data = [{name: 'fruit'}, {name: 'vegetable'}];
|
|
16
21
|
const Fixture = () => {
|
|
@@ -40,4 +45,68 @@ describe('Table.Predicate', () => {
|
|
|
40
45
|
await user.click(screen.getByRole('option', {name: 'First'}));
|
|
41
46
|
expect(screen.getByRole('button', {name: '1', current: 'page'})).toBeVisible();
|
|
42
47
|
});
|
|
48
|
+
|
|
49
|
+
describe('when url sync is activated', () => {
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
window.history.replaceState(null, '', '/');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('sets the current predicate value in the url using the predicate id as key', async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
const data = [{name: 'fruit'}, {name: 'vegetable'}];
|
|
57
|
+
const Fixture = () => {
|
|
58
|
+
const store = useTable<RowData>({
|
|
59
|
+
initialState: {predicates: {rank: 'ALL'}},
|
|
60
|
+
syncWithUrl: true,
|
|
61
|
+
});
|
|
62
|
+
return (
|
|
63
|
+
<Table store={store} data={data} columns={columns}>
|
|
64
|
+
<Table.Header>
|
|
65
|
+
<Table.Predicate
|
|
66
|
+
id="rank"
|
|
67
|
+
label="Rank"
|
|
68
|
+
data={[
|
|
69
|
+
{value: 'ALL', label: 'All'},
|
|
70
|
+
{value: 'first', label: 'First'},
|
|
71
|
+
{value: 'second', label: 'Second'},
|
|
72
|
+
]}
|
|
73
|
+
/>
|
|
74
|
+
</Table.Header>
|
|
75
|
+
</Table>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
render(<Fixture />);
|
|
79
|
+
await user.click(screen.getByRole('textbox', {name: 'Rank'}));
|
|
80
|
+
await user.click(screen.getByRole('option', {name: 'First'}));
|
|
81
|
+
expect(window.location.search).toBe('?rank=first');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('determines the initial predicate value from the url', async () => {
|
|
85
|
+
window.history.replaceState(null, '', '?rank=second');
|
|
86
|
+
const data = [{name: 'fruit'}, {name: 'vegetable'}];
|
|
87
|
+
const Fixture = () => {
|
|
88
|
+
const store = useTable<RowData>({
|
|
89
|
+
initialState: {predicates: {rank: 'ALL'}},
|
|
90
|
+
syncWithUrl: true,
|
|
91
|
+
});
|
|
92
|
+
return (
|
|
93
|
+
<Table store={store} data={data} columns={columns}>
|
|
94
|
+
<Table.Header>
|
|
95
|
+
<Table.Predicate
|
|
96
|
+
id="rank"
|
|
97
|
+
label="Rank"
|
|
98
|
+
data={[
|
|
99
|
+
{value: 'ALL', label: 'All'},
|
|
100
|
+
{value: 'first', label: 'First'},
|
|
101
|
+
{value: 'second', label: 'Second'},
|
|
102
|
+
]}
|
|
103
|
+
/>
|
|
104
|
+
</Table.Header>
|
|
105
|
+
</Table>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
render(<Fixture />);
|
|
109
|
+
expect(screen.getByRole('textbox', {name: 'Rank'})).toHaveValue('Second');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
43
112
|
});
|
|
@@ -56,4 +56,39 @@ describe('Th', () => {
|
|
|
56
56
|
expect(sortedDescRowHeader).toBeVisible();
|
|
57
57
|
await user.click(sortedDescRowHeader);
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('when url sync is activated', () => {
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
window.history.replaceState(null, '', '/');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('sets the sort column and direction in the url', async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
const data: RowData[] = [
|
|
68
|
+
{name: 'apple', type: 'fruit', colors: ['red', 'green']},
|
|
69
|
+
{name: 'potato', type: 'vegetable', colors: ['brown', 'blue', 'yellow']},
|
|
70
|
+
];
|
|
71
|
+
const Fixture = () => {
|
|
72
|
+
const store = useTable<RowData>({syncWithUrl: true});
|
|
73
|
+
return <Table store={store} data={data} columns={columns} />;
|
|
74
|
+
};
|
|
75
|
+
render(<Fixture />);
|
|
76
|
+
await user.click(screen.getByRole('columnheader', {name: /name doubleArrowHead/i}));
|
|
77
|
+
expect(window.location.search).toBe('?sortBy=name.asc');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('determines the initial visible columns from the url', () => {
|
|
81
|
+
window.history.replaceState(null, '', '?sortBy=name.desc');
|
|
82
|
+
const data: RowData[] = [
|
|
83
|
+
{name: 'apple', type: 'fruit', colors: ['red', 'green']},
|
|
84
|
+
{name: 'potato', type: 'vegetable', colors: ['brown', 'blue', 'yellow']},
|
|
85
|
+
];
|
|
86
|
+
const Fixture = () => {
|
|
87
|
+
const store = useTable<RowData>({syncWithUrl: true});
|
|
88
|
+
return <Table store={store} data={data} columns={columns} />;
|
|
89
|
+
};
|
|
90
|
+
render(<Fixture />);
|
|
91
|
+
expect(screen.getByRole('columnheader', {name: /name arrowDown/i})).toBeVisible();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
59
94
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {act, renderHook} from '@test-utils';
|
|
2
|
+
import {useUrlSyncedState} from '../use-url-synced-state';
|
|
3
|
+
|
|
4
|
+
describe('useUrlSyncedState', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
window.history.replaceState(null, '', '/');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('serializes the state value as url parameter when the state changes', () => {
|
|
10
|
+
const {result} = renderHook(() =>
|
|
11
|
+
useUrlSyncedState({
|
|
12
|
+
initialState: '',
|
|
13
|
+
serializer: (state) => [['key', state]],
|
|
14
|
+
deserializer: (params) => params.get('key') ?? '',
|
|
15
|
+
sync: true,
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
act(() => result.current[1]('value'));
|
|
19
|
+
expect(result.current[0]).toBe('value');
|
|
20
|
+
expect(window.location.search).toBe('?key=value');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('allows to serialize the state value into multiple parameters', () => {
|
|
24
|
+
const {result} = renderHook(() =>
|
|
25
|
+
useUrlSyncedState({
|
|
26
|
+
initialState: {key1: '', key2: ''},
|
|
27
|
+
serializer: (state) => [
|
|
28
|
+
['key1', state.key1],
|
|
29
|
+
['key2', state.key2],
|
|
30
|
+
],
|
|
31
|
+
deserializer: (params) => ({
|
|
32
|
+
key1: params.get('key1') ?? '',
|
|
33
|
+
key2: params.get('key2') ?? '',
|
|
34
|
+
}),
|
|
35
|
+
sync: true,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
act(() => result.current[1]({key1: 'value1', key2: 'value2'}));
|
|
39
|
+
expect(window.location.search).toBe('?key1=value1&key2=value2');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('removes the parameter from the url if the state serializes to the same value as the initial state', () => {
|
|
43
|
+
const {result} = renderHook(() =>
|
|
44
|
+
useUrlSyncedState({
|
|
45
|
+
initialState: 'initial',
|
|
46
|
+
serializer: (state) => [['key', state]],
|
|
47
|
+
deserializer: (params) => params.get('key') ?? '',
|
|
48
|
+
sync: true,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
act(() => result.current[1]('value'));
|
|
52
|
+
expect(window.location.search).toBe('?key=value');
|
|
53
|
+
act(() => result.current[1]('initial'));
|
|
54
|
+
expect(window.location.search).toBe('');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('removes the parameter from the url if the state serializes to the empty string', () => {
|
|
58
|
+
const {result} = renderHook(() =>
|
|
59
|
+
useUrlSyncedState({
|
|
60
|
+
initialState: 'initial',
|
|
61
|
+
serializer: (state) => [['key', state]],
|
|
62
|
+
deserializer: (params) => params.get('key') ?? '',
|
|
63
|
+
sync: true,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
act(() => result.current[1]('value'));
|
|
67
|
+
expect(window.location.search).toBe('?key=value');
|
|
68
|
+
act(() => result.current[1](''));
|
|
69
|
+
expect(window.location.search).toBe('');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('does not sync with the url if the sync parameter is set to false', () => {
|
|
73
|
+
const {result} = renderHook(() =>
|
|
74
|
+
useUrlSyncedState({
|
|
75
|
+
initialState: '',
|
|
76
|
+
serializer: (state) => [['key', state]],
|
|
77
|
+
deserializer: (params) => params.get('key') ?? '',
|
|
78
|
+
sync: false,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
act(() => result.current[1]('value'));
|
|
82
|
+
expect(result.current[0]).toBe('value');
|
|
83
|
+
expect(window.location.search).toBe('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('derives the initial state from the url on first render', () => {
|
|
87
|
+
window.history.replaceState(null, '', '?key=value');
|
|
88
|
+
|
|
89
|
+
const {result} = renderHook(() =>
|
|
90
|
+
useUrlSyncedState({
|
|
91
|
+
initialState: 'initial',
|
|
92
|
+
serializer: (state) => [['key', state]],
|
|
93
|
+
deserializer: (params) => params.get('key') ?? '',
|
|
94
|
+
sync: true,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(result.current[0]).toBe('value');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not derive the initial state from the url on first render if sync option is false', () => {
|
|
101
|
+
window.history.replaceState(null, '', '?key=value');
|
|
102
|
+
|
|
103
|
+
const {result} = renderHook(() =>
|
|
104
|
+
useUrlSyncedState({
|
|
105
|
+
initialState: 'initial',
|
|
106
|
+
serializer: (state) => [['key', state]],
|
|
107
|
+
deserializer: (params) => params.get('key') ?? '',
|
|
108
|
+
sync: false,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
expect(result.current[0]).toBe('initial');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -3,13 +3,12 @@ import {type ExpandedState, type PaginationState, type SortingState} from '@tans
|
|
|
3
3
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
4
4
|
import {Dispatch, SetStateAction, useCallback, useMemo, useState} from 'react';
|
|
5
5
|
import {type DateRangePickerValue} from '../date-range-picker';
|
|
6
|
+
import {useUrlSyncedState} from './use-url-synced-state';
|
|
6
7
|
|
|
7
8
|
// Create a deeply optional version of another type
|
|
8
|
-
type
|
|
9
|
-
?
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
: T;
|
|
9
|
+
type DeepPartial<T> = {
|
|
10
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
11
|
+
};
|
|
13
12
|
|
|
14
13
|
export interface TableState<TData = unknown> {
|
|
15
14
|
/**
|
|
@@ -166,7 +165,7 @@ export interface UseTableOptions<TData = unknown> {
|
|
|
166
165
|
/**
|
|
167
166
|
* Initial state of the table.
|
|
168
167
|
*/
|
|
169
|
-
initialState?:
|
|
168
|
+
initialState?: DeepPartial<TableState<TData>>;
|
|
170
169
|
/**
|
|
171
170
|
* Whether rows can be selected.
|
|
172
171
|
*
|
|
@@ -186,12 +185,19 @@ export interface UseTableOptions<TData = unknown> {
|
|
|
186
185
|
* @default false
|
|
187
186
|
*/
|
|
188
187
|
forceSelection?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Whether to sync the table state with the URL.
|
|
190
|
+
*
|
|
191
|
+
* @default false
|
|
192
|
+
*/
|
|
193
|
+
syncWithUrl?: boolean;
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
const defaultOptions: UseTableOptions = {
|
|
192
197
|
enableRowSelection: true,
|
|
193
198
|
enableMultiRowSelection: false,
|
|
194
199
|
forceSelection: false,
|
|
200
|
+
syncWithUrl: false,
|
|
195
201
|
};
|
|
196
202
|
|
|
197
203
|
const defaultState: Partial<TableState> = {
|
|
@@ -213,21 +219,120 @@ export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): Table
|
|
|
213
219
|
const options = defaultsDeep({}, userOptions, defaultOptions) as UseTableOptions<TData>;
|
|
214
220
|
const initialState = defaultsDeep({}, options.initialState, defaultState) as TableState<TData>;
|
|
215
221
|
|
|
216
|
-
|
|
222
|
+
// synced with url
|
|
223
|
+
const [pagination, setPagination] = useUrlSyncedState<TableState<TData>['pagination']>({
|
|
224
|
+
initialState: initialState.pagination,
|
|
225
|
+
serializer: ({pageIndex, pageSize}) => [
|
|
226
|
+
['page', (pageIndex + 1).toString()],
|
|
227
|
+
['pageSize', pageSize.toString()],
|
|
228
|
+
],
|
|
229
|
+
deserializer: (params) =>
|
|
230
|
+
defaultsDeep(
|
|
231
|
+
{
|
|
232
|
+
pageIndex: params.get('page') ? parseInt(params.get('page'), 10) - 1 : undefined,
|
|
233
|
+
pageSize: params.get('pageSize') ? parseInt(params.get('pageSize'), 10) : undefined,
|
|
234
|
+
},
|
|
235
|
+
initialState.pagination,
|
|
236
|
+
),
|
|
237
|
+
sync: options.syncWithUrl,
|
|
238
|
+
});
|
|
239
|
+
const [sorting, setSorting] = useUrlSyncedState<TableState<TData>['sorting']>({
|
|
240
|
+
initialState: initialState.sorting,
|
|
241
|
+
serializer: (_sorting) => [
|
|
242
|
+
['sortBy', _sorting.map(({id, desc}) => `${id}.${desc ? 'desc' : 'asc'}`).join(',')],
|
|
243
|
+
],
|
|
244
|
+
deserializer: (params) => {
|
|
245
|
+
if (!params.has('sortBy')) {
|
|
246
|
+
return initialState.sorting;
|
|
247
|
+
}
|
|
248
|
+
const sorts = params.get('sortBy')?.split(',') ?? [];
|
|
249
|
+
return sorts.map((sort) => {
|
|
250
|
+
const [id, order] = sort.split('.');
|
|
251
|
+
return {id, desc: order === 'desc'};
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
sync: options.syncWithUrl,
|
|
255
|
+
});
|
|
256
|
+
const [globalFilter, setGlobalFilter] = useUrlSyncedState<TableState<TData>['globalFilter']>({
|
|
257
|
+
initialState: initialState.globalFilter,
|
|
258
|
+
serializer: (filter) => [['filter', filter]],
|
|
259
|
+
deserializer: (params) => params.get('filter') ?? initialState.globalFilter,
|
|
260
|
+
sync: options.syncWithUrl,
|
|
261
|
+
});
|
|
262
|
+
const [predicates, setPredicates] = useUrlSyncedState<TableState<TData>['predicates']>({
|
|
263
|
+
initialState: initialState.predicates,
|
|
264
|
+
serializer: (_predicates) => Object.entries(_predicates).map(([key, value]) => [key, value]),
|
|
265
|
+
deserializer: (params) =>
|
|
266
|
+
Object.keys(initialState.predicates).reduce(
|
|
267
|
+
(acc, predicateKey) => {
|
|
268
|
+
acc[predicateKey] = params.get(predicateKey) ?? initialState.predicates[predicateKey];
|
|
269
|
+
return acc;
|
|
270
|
+
},
|
|
271
|
+
{} as TableState<TData>['predicates'],
|
|
272
|
+
),
|
|
273
|
+
sync: options.syncWithUrl,
|
|
274
|
+
});
|
|
275
|
+
const [layout, setLayout] = useUrlSyncedState<TableState<TData>['layout']>({
|
|
276
|
+
initialState: initialState.layout,
|
|
277
|
+
serializer: (_layout) => [['layout', _layout]],
|
|
278
|
+
deserializer: (params) => params.get('layout') ?? initialState.layout,
|
|
279
|
+
sync: options.syncWithUrl,
|
|
280
|
+
});
|
|
281
|
+
const [dateRange, setDateRange] = useUrlSyncedState<TableState<TData>['dateRange']>({
|
|
282
|
+
initialState: initialState.dateRange,
|
|
283
|
+
serializer: ([from, to]) => [
|
|
284
|
+
['from', from?.toISOString() ?? ''],
|
|
285
|
+
['to', to?.toISOString() ?? ''],
|
|
286
|
+
],
|
|
287
|
+
deserializer: (params) => [
|
|
288
|
+
params.get('from') ? new Date(params.get('from') as string) : initialState.dateRange[0],
|
|
289
|
+
params.get('to') ? new Date(params.get('to') as string) : initialState.dateRange[1],
|
|
290
|
+
],
|
|
291
|
+
sync: options.syncWithUrl,
|
|
292
|
+
});
|
|
293
|
+
const [columnVisibility, setColumnVisibility] = useUrlSyncedState<TableState<TData>['columnVisibility']>({
|
|
294
|
+
initialState: initialState.columnVisibility,
|
|
295
|
+
serializer: (columns) => [
|
|
296
|
+
[
|
|
297
|
+
'show',
|
|
298
|
+
Object.entries(columns)
|
|
299
|
+
.filter(([, visible]) => visible === true)
|
|
300
|
+
.map(([columnName]) => columnName)
|
|
301
|
+
.join(','),
|
|
302
|
+
],
|
|
303
|
+
[
|
|
304
|
+
'hide',
|
|
305
|
+
Object.entries(columns)
|
|
306
|
+
.filter(([, visible]) => visible === false)
|
|
307
|
+
.map(([columnName]) => columnName)
|
|
308
|
+
.join(','),
|
|
309
|
+
],
|
|
310
|
+
],
|
|
311
|
+
deserializer: (params) => {
|
|
312
|
+
if (!params.has('show') && !params.has('hide')) {
|
|
313
|
+
return initialState.columnVisibility;
|
|
314
|
+
}
|
|
315
|
+
const visible = params.get('show')?.split(',') ?? [];
|
|
316
|
+
const invisible = params.get('hide')?.split(',') ?? [];
|
|
317
|
+
const columns = {} as TableState<TData>['columnVisibility'];
|
|
318
|
+
visible.forEach((column) => {
|
|
319
|
+
columns[column] = true;
|
|
320
|
+
});
|
|
321
|
+
invisible.forEach((column) => {
|
|
322
|
+
columns[column] = false;
|
|
323
|
+
});
|
|
324
|
+
return columns;
|
|
325
|
+
},
|
|
326
|
+
sync: options.syncWithUrl,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// unsynced
|
|
217
330
|
const [totalEntries, _setTotalEntries] = useState<TableState<TData>['totalEntries']>(initialState.totalEntries);
|
|
218
331
|
const [unfilteredTotalEntries, setUnfilteredTotalEntries] = useState<TableState<TData>['totalEntries']>(
|
|
219
332
|
initialState.totalEntries,
|
|
220
333
|
);
|
|
221
|
-
const [sorting, setSorting] = useState<TableState<TData>['sorting']>(initialState.sorting);
|
|
222
|
-
const [globalFilter, setGlobalFilter] = useState<TableState<TData>['globalFilter']>(initialState.globalFilter);
|
|
223
334
|
const [expanded, setExpanded] = useState<TableState<TData>['expanded']>(initialState.expanded);
|
|
224
|
-
const [predicates, setPredicates] = useState<TableState<TData>['predicates']>(initialState.predicates);
|
|
225
|
-
const [layout, setLayout] = useState<TableState<TData>['layout']>(initialState.layout);
|
|
226
|
-
const [dateRange, setDateRange] = useState<TableState<TData>['dateRange']>(initialState.dateRange);
|
|
227
335
|
const [rowSelection, setRowSelection] = useState<TableState<TData>['rowSelection']>(initialState.rowSelection);
|
|
228
|
-
const [columnVisibility, setColumnVisibility] = useState<TableState<TData>['columnVisibility']>(
|
|
229
|
-
initialState.columnVisibility,
|
|
230
|
-
);
|
|
231
336
|
|
|
232
337
|
const isFiltered =
|
|
233
338
|
!!globalFilter ||
|