@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.
Files changed (38) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/.turbo/turbo-test.log +48 -45
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cjs/components/date-range-picker/DateRangePickerInlineCalendar.d.ts.map +1 -1
  5. package/dist/cjs/components/date-range-picker/DateRangePickerInlineCalendar.js +21 -3
  6. package/dist/cjs/components/date-range-picker/DateRangePickerInlineCalendar.js.map +1 -1
  7. package/dist/cjs/components/table/use-table.d.ts +10 -4
  8. package/dist/cjs/components/table/use-table.d.ts.map +1 -1
  9. package/dist/cjs/components/table/use-table.js +184 -12
  10. package/dist/cjs/components/table/use-table.js.map +1 -1
  11. package/dist/cjs/components/table/use-url-synced-state.d.ts +29 -0
  12. package/dist/cjs/components/table/use-url-synced-state.d.ts.map +1 -0
  13. package/dist/cjs/components/table/use-url-synced-state.js +58 -0
  14. package/dist/cjs/components/table/use-url-synced-state.js.map +1 -0
  15. package/dist/esm/components/date-range-picker/DateRangePickerInlineCalendar.d.ts.map +1 -1
  16. package/dist/esm/components/date-range-picker/DateRangePickerInlineCalendar.js +15 -2
  17. package/dist/esm/components/date-range-picker/DateRangePickerInlineCalendar.js.map +1 -1
  18. package/dist/esm/components/table/use-table.d.ts +10 -4
  19. package/dist/esm/components/table/use-table.d.ts.map +1 -1
  20. package/dist/esm/components/table/use-table.js +127 -8
  21. package/dist/esm/components/table/use-table.js.map +1 -1
  22. package/dist/esm/components/table/use-url-synced-state.d.ts +29 -0
  23. package/dist/esm/components/table/use-url-synced-state.d.ts.map +1 -0
  24. package/dist/esm/components/table/use-url-synced-state.js +41 -0
  25. package/dist/esm/components/table/use-url-synced-state.js.map +1 -0
  26. package/package.json +8 -8
  27. package/src/components/date-range-picker/DateRangePickerInlineCalendar.tsx +15 -1
  28. package/src/components/date-range-picker/__tests__/DateRangePickerInlineCalendar.spec.tsx +6 -4
  29. package/src/components/table/__tests__/TableColumnsSelector.spec.tsx +51 -0
  30. package/src/components/table/__tests__/TableDateRangePicker.spec.tsx +53 -1
  31. package/src/components/table/__tests__/TableFilter.spec.tsx +43 -0
  32. package/src/components/table/__tests__/TablePagination.spec.tsx +54 -0
  33. package/src/components/table/__tests__/TablePerPage.spec.tsx +55 -0
  34. package/src/components/table/__tests__/TablePredicate.spec.tsx +69 -0
  35. package/src/components/table/__tests__/Th.spec.tsx +35 -0
  36. package/src/components/table/__tests__/use-url-synced-state.unit.spec.ts +113 -0
  37. package/src/components/table/use-table.ts +120 -15
  38. 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 PartialDeep<T> = T extends object
9
- ? {
10
- [P in keyof T]?: PartialDeep<T[P]>;
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?: PartialDeep<TableState<TData>>;
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
- const [pagination, setPagination] = useState<TableState<TData>['pagination']>(initialState.pagination);
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 ||