@webitel/ui-datalist 1.1.65 → 1.1.66
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/package.json +1 -1
- package/src/modules/filters/__tests__/createTableFiltersStore.spec.ts +143 -0
- package/src/modules/filters/createTableFiltersStore.ts +23 -5
- package/src/modules/headers/__tests__/createTableHeadersStore.spec.ts +169 -0
- package/src/modules/headers/createTableHeadersStore.ts +26 -7
- package/src/modules/pagination/__tests__/createTablePaginationStore.spec.ts +134 -0
- package/src/modules/pagination/createTablePaginationStore.ts +32 -4
- package/src/modules/persist/PersistedStorage.types.ts +14 -0
- package/src/modules/persist/__tests__/useLocalStoragePersistedStorage.spec.ts +66 -0
- package/src/modules/persist/__tests__/usePersistedStorage.spec.ts +357 -0
- package/src/modules/persist/__tests__/useRoutePersistedStorage.spec.ts +145 -0
- package/src/modules/persist/usePersistedStorage.ts +93 -60
- package/src/modules/persist/useRoutePersistedStorage.ts +40 -18
- package/src/modules/table/createTableStore.store.ts +37 -0
- package/types/.tsbuildinfo +1 -1
- package/types/modules/filter-presets/stores/createFilterPresetsStore.d.ts +1 -0
- package/types/modules/filters/createTableFiltersStore.d.ts +2 -0
- package/types/modules/headers/createTableHeadersStore.d.ts +2 -0
- package/types/modules/pagination/createTablePaginationStore.d.ts +2 -0
- package/types/modules/permissions-page/stores/createPermissionsStore.d.ts +2 -0
- package/types/modules/persist/PersistedStorage.types.d.ts +13 -0
- package/types/modules/table/createTableStore.store.d.ts +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { createApp } from 'vue';
|
|
3
|
+
import { createMemoryHistory, createRouter, type Router } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
import { tableFiltersStoreBody } from '../createTableFiltersStore';
|
|
6
|
+
|
|
7
|
+
const routes = [
|
|
8
|
+
{
|
|
9
|
+
path: '/cases',
|
|
10
|
+
name: 'cases',
|
|
11
|
+
component: {
|
|
12
|
+
template: '<div />',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const namespace = 'cases';
|
|
18
|
+
|
|
19
|
+
describe('tableFiltersStoreBody', () => {
|
|
20
|
+
let router: Router;
|
|
21
|
+
let runWithRouter: <T>(fn: () => T) => T;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
localStorage.clear();
|
|
25
|
+
|
|
26
|
+
router = createRouter({
|
|
27
|
+
history: createMemoryHistory(),
|
|
28
|
+
routes,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const app = createApp({});
|
|
32
|
+
app.use(router);
|
|
33
|
+
|
|
34
|
+
await router.push('/cases');
|
|
35
|
+
await router.isReady();
|
|
36
|
+
|
|
37
|
+
runWithRouter = (fn) => app.runWithContext(fn);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const setUpStore = async () => {
|
|
41
|
+
const store = tableFiltersStoreBody(namespace);
|
|
42
|
+
await runWithRouter(() => store.setupPersistence());
|
|
43
|
+
return store;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
it('starts with no filters', () => {
|
|
47
|
+
const store = tableFiltersStoreBody(namespace);
|
|
48
|
+
|
|
49
|
+
expect(store.filtersList.value).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('adds, updates and deletes a filter', () => {
|
|
53
|
+
const store = tableFiltersStoreBody(namespace);
|
|
54
|
+
|
|
55
|
+
store.addFilter({
|
|
56
|
+
name: 'status',
|
|
57
|
+
value: 'open',
|
|
58
|
+
});
|
|
59
|
+
expect(store.hasFilter('status')).toBe(true);
|
|
60
|
+
|
|
61
|
+
store.updateFilter({
|
|
62
|
+
name: 'status',
|
|
63
|
+
value: 'closed',
|
|
64
|
+
});
|
|
65
|
+
expect(store.filtersManager.getFilter('status')?.value).toBe('closed');
|
|
66
|
+
|
|
67
|
+
store.deleteFilter({
|
|
68
|
+
name: 'status',
|
|
69
|
+
});
|
|
70
|
+
expect(store.hasFilter('status')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('keeps searchMode', () => {
|
|
74
|
+
const store = tableFiltersStoreBody(namespace);
|
|
75
|
+
|
|
76
|
+
store.updateSearchMode('subject');
|
|
77
|
+
|
|
78
|
+
expect(store.searchMode.value).toBe('subject');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('persistence', () => {
|
|
82
|
+
it('restores filters from the route query', async () => {
|
|
83
|
+
await router.push({
|
|
84
|
+
name: 'cases',
|
|
85
|
+
query: {
|
|
86
|
+
filters: JSON.stringify({
|
|
87
|
+
status_val: 'open',
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const store = await setUpStore();
|
|
93
|
+
|
|
94
|
+
expect(store.filtersManager.getFilter('status')?.value).toBe('open');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('writes an added filter into the route query', async () => {
|
|
98
|
+
const store = await setUpStore();
|
|
99
|
+
|
|
100
|
+
store.addFilter({
|
|
101
|
+
name: 'status',
|
|
102
|
+
value: 'open',
|
|
103
|
+
});
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
105
|
+
|
|
106
|
+
expect(router.currentRoute.value.query.filters).toContain('open');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('keeps searchMode out of the route query', async () => {
|
|
110
|
+
const store = await setUpStore();
|
|
111
|
+
|
|
112
|
+
store.updateSearchMode('subject');
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
114
|
+
|
|
115
|
+
expect(router.currentRoute.value.query.searchMode).toBeUndefined();
|
|
116
|
+
expect(localStorage.getItem(`${namespace}/searchMode`)).toBe('subject');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('syncPersistence', () => {
|
|
121
|
+
it('publishes the applied filters', async () => {
|
|
122
|
+
const store = await setUpStore();
|
|
123
|
+
store.addFilter({
|
|
124
|
+
name: 'status',
|
|
125
|
+
value: 'open',
|
|
126
|
+
});
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
128
|
+
await router.push('/cases');
|
|
129
|
+
|
|
130
|
+
await runWithRouter(() => store.syncPersistence());
|
|
131
|
+
|
|
132
|
+
expect(router.currentRoute.value.query.filters).toContain('open');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('publishes nothing while no filter is applied', async () => {
|
|
136
|
+
const store = await setUpStore();
|
|
137
|
+
|
|
138
|
+
await runWithRouter(() => store.syncPersistence());
|
|
139
|
+
|
|
140
|
+
expect(router.currentRoute.value.query).toEqual({});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { computed, reactive, ref } from 'vue';
|
|
2
2
|
|
|
3
3
|
import { createDatalistStore } from '../_shared/createDatalistStore';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type PersistedStorageController,
|
|
6
|
+
PersistedStorageType,
|
|
7
|
+
} from '../persist/PersistedStorage.types';
|
|
5
8
|
import { usePersistedStorage } from '../persist/usePersistedStorage';
|
|
6
9
|
import type { Identifiable } from '../types/createDatalistStore.types';
|
|
7
10
|
import type { useTableStoreConfig } from '../types/tableStore.types';
|
|
@@ -40,8 +43,10 @@ export const tableFiltersStoreBody = (
|
|
|
40
43
|
|
|
41
44
|
const filtersList = computed(() => filtersManager.getFiltersList());
|
|
42
45
|
|
|
46
|
+
let persistedStorageControllers: PersistedStorageController[] = [];
|
|
47
|
+
|
|
43
48
|
const setupPersistence = () => {
|
|
44
|
-
const
|
|
49
|
+
const filtersStorage = usePersistedStorage({
|
|
45
50
|
name: 'filters',
|
|
46
51
|
|
|
47
52
|
value: computed(
|
|
@@ -75,7 +80,7 @@ export const tableFiltersStoreBody = (
|
|
|
75
80
|
},
|
|
76
81
|
});
|
|
77
82
|
|
|
78
|
-
const
|
|
83
|
+
const searchModeStorage = usePersistedStorage({
|
|
79
84
|
name: 'searchMode',
|
|
80
85
|
value: searchMode,
|
|
81
86
|
storages: [
|
|
@@ -95,12 +100,24 @@ export const tableFiltersStoreBody = (
|
|
|
95
100
|
},
|
|
96
101
|
});
|
|
97
102
|
|
|
103
|
+
persistedStorageControllers = [
|
|
104
|
+
filtersStorage,
|
|
105
|
+
searchModeStorage,
|
|
106
|
+
];
|
|
107
|
+
|
|
98
108
|
return Promise.all([
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
filtersStorage.restore(),
|
|
110
|
+
searchModeStorage.restore(),
|
|
101
111
|
]);
|
|
102
112
|
};
|
|
103
113
|
|
|
114
|
+
/* sequentially: every route write is a router.replace() on top of the current query */
|
|
115
|
+
const syncPersistence = async () => {
|
|
116
|
+
for (const controller of persistedStorageControllers) {
|
|
117
|
+
await controller.sync();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
104
121
|
return {
|
|
105
122
|
filtersManager,
|
|
106
123
|
isRestoring,
|
|
@@ -116,6 +133,7 @@ export const tableFiltersStoreBody = (
|
|
|
116
133
|
updateSearchMode,
|
|
117
134
|
|
|
118
135
|
setupPersistence,
|
|
136
|
+
syncPersistence,
|
|
119
137
|
};
|
|
120
138
|
};
|
|
121
139
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { createApp } from 'vue';
|
|
3
|
+
import { createMemoryHistory, createRouter, type Router } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
import type { DatalistTableHeader } from '../../types/tableStore.types';
|
|
6
|
+
import { tableHeadersStoreBody } from '../createTableHeadersStore';
|
|
7
|
+
|
|
8
|
+
const routes = [
|
|
9
|
+
{
|
|
10
|
+
path: '/cases',
|
|
11
|
+
name: 'cases',
|
|
12
|
+
component: {
|
|
13
|
+
template: '<div />',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const id = 'cases/headers';
|
|
19
|
+
|
|
20
|
+
const rawHeaders = [
|
|
21
|
+
{
|
|
22
|
+
field: 'name',
|
|
23
|
+
show: true,
|
|
24
|
+
sort: null,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
field: 'subject',
|
|
28
|
+
show: true,
|
|
29
|
+
sort: null,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
field: 'createdAt',
|
|
33
|
+
show: false,
|
|
34
|
+
sort: null,
|
|
35
|
+
},
|
|
36
|
+
] as DatalistTableHeader[];
|
|
37
|
+
|
|
38
|
+
/* route writes are queued, so several ticks are needed for all of them to land */
|
|
39
|
+
const flushWrites = async () => {
|
|
40
|
+
for (let i = 0; i < 5; i += 1) {
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createStore = () =>
|
|
46
|
+
tableHeadersStoreBody({
|
|
47
|
+
rawHeaders: rawHeaders.map((header) => ({
|
|
48
|
+
...header,
|
|
49
|
+
})),
|
|
50
|
+
id,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('tableHeadersStoreBody', () => {
|
|
54
|
+
let router: Router;
|
|
55
|
+
let runWithRouter: <T>(fn: () => T) => T;
|
|
56
|
+
|
|
57
|
+
beforeEach(async () => {
|
|
58
|
+
localStorage.clear();
|
|
59
|
+
|
|
60
|
+
router = createRouter({
|
|
61
|
+
history: createMemoryHistory(),
|
|
62
|
+
routes,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const app = createApp({});
|
|
66
|
+
app.use(router);
|
|
67
|
+
|
|
68
|
+
await router.push('/cases');
|
|
69
|
+
await router.isReady();
|
|
70
|
+
|
|
71
|
+
runWithRouter = (fn) => app.runWithContext(fn);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const setUpStore = async () => {
|
|
75
|
+
const store = createStore();
|
|
76
|
+
await runWithRouter(() => store.setupPersistence());
|
|
77
|
+
return store;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
it('exposes the shown headers and their fields', () => {
|
|
81
|
+
const store = createStore();
|
|
82
|
+
|
|
83
|
+
expect(store.shownHeaders.value.map(({ field }) => field)).toEqual([
|
|
84
|
+
'name',
|
|
85
|
+
'subject',
|
|
86
|
+
]);
|
|
87
|
+
expect(store.fields.value).toEqual([
|
|
88
|
+
'name',
|
|
89
|
+
'subject',
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('has no sort by default', () => {
|
|
94
|
+
expect(createStore().sort.value).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('encodes the sorted column into a sort query', () => {
|
|
98
|
+
const store = createStore();
|
|
99
|
+
|
|
100
|
+
store.updateSort(store.headers.value[0], 'desc');
|
|
101
|
+
|
|
102
|
+
expect(store.sort.value).toBe('-name');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('collects the widths of resized columns', () => {
|
|
106
|
+
const store = createStore();
|
|
107
|
+
|
|
108
|
+
store.columnResize({
|
|
109
|
+
columnName: 'name',
|
|
110
|
+
columnWidth: '100px',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(store.columnWidths.value).toEqual({
|
|
114
|
+
name: '100px',
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('persistence', () => {
|
|
119
|
+
it('restores shown fields from the route query', async () => {
|
|
120
|
+
await router.push({
|
|
121
|
+
name: 'cases',
|
|
122
|
+
query: {
|
|
123
|
+
fields: 'createdAt',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const store = await setUpStore();
|
|
128
|
+
|
|
129
|
+
expect(store.fields.value).toEqual([
|
|
130
|
+
'createdAt',
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('writes a changed sort into the route query', async () => {
|
|
135
|
+
const store = await setUpStore();
|
|
136
|
+
|
|
137
|
+
store.updateSort(store.headers.value[0], 'asc');
|
|
138
|
+
await flushWrites();
|
|
139
|
+
|
|
140
|
+
expect(router.currentRoute.value.query.sort).toBe('+name');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('syncPersistence', () => {
|
|
145
|
+
it('publishes a customized column set', async () => {
|
|
146
|
+
const store = await setUpStore();
|
|
147
|
+
store.updateShownHeaders(
|
|
148
|
+
store.headers.value.map((header) => ({
|
|
149
|
+
...header,
|
|
150
|
+
show: header.field === 'name',
|
|
151
|
+
})),
|
|
152
|
+
);
|
|
153
|
+
await flushWrites();
|
|
154
|
+
await router.push('/cases');
|
|
155
|
+
|
|
156
|
+
await runWithRouter(() => store.syncPersistence());
|
|
157
|
+
|
|
158
|
+
expect(router.currentRoute.value.query.fields).toBe('name');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('publishes nothing while the columns are still default', async () => {
|
|
162
|
+
const store = await setUpStore();
|
|
163
|
+
|
|
164
|
+
await runWithRouter(() => store.syncPersistence());
|
|
165
|
+
|
|
166
|
+
expect(router.currentRoute.value.query).toEqual({});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -4,7 +4,10 @@ import { SortSymbols } from '@webitel/ui-sdk/scripts/sortQueryAdapters';
|
|
|
4
4
|
import { computed, nextTick, ref } from 'vue';
|
|
5
5
|
|
|
6
6
|
import { createDatalistStore } from '../_shared/createDatalistStore';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type PersistedStorageController,
|
|
9
|
+
PersistedStorageType,
|
|
10
|
+
} from '../persist/PersistedStorage.types';
|
|
8
11
|
import { usePersistedStorage } from '../persist/usePersistedStorage';
|
|
9
12
|
import type { Identifiable } from '../types/createDatalistStore.types';
|
|
10
13
|
import type {
|
|
@@ -223,8 +226,10 @@ export const tableHeadersStoreBody = ({
|
|
|
223
226
|
});
|
|
224
227
|
};
|
|
225
228
|
|
|
229
|
+
let persistedStorageControllers: PersistedStorageController[] = [];
|
|
230
|
+
|
|
226
231
|
const setupPersistence = async () => {
|
|
227
|
-
const
|
|
232
|
+
const fieldsStorage = usePersistedStorage({
|
|
228
233
|
name: 'fields',
|
|
229
234
|
value: fields,
|
|
230
235
|
storages: [
|
|
@@ -247,12 +252,12 @@ export const tableHeadersStoreBody = ({
|
|
|
247
252
|
},
|
|
248
253
|
});
|
|
249
254
|
|
|
250
|
-
const
|
|
255
|
+
const sortStorage = usePersistedStorage({
|
|
251
256
|
name: 'sort',
|
|
252
257
|
value: sort,
|
|
253
258
|
});
|
|
254
259
|
|
|
255
|
-
const
|
|
260
|
+
const columnWidthsStorage = usePersistedStorage({
|
|
256
261
|
name: 'columnWidths',
|
|
257
262
|
value: columnWidths,
|
|
258
263
|
storages: [
|
|
@@ -278,13 +283,26 @@ export const tableHeadersStoreBody = ({
|
|
|
278
283
|
},
|
|
279
284
|
});
|
|
280
285
|
|
|
286
|
+
persistedStorageControllers = [
|
|
287
|
+
fieldsStorage,
|
|
288
|
+
sortStorage,
|
|
289
|
+
columnWidthsStorage,
|
|
290
|
+
];
|
|
291
|
+
|
|
281
292
|
return Promise.allSettled([
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
293
|
+
fieldsStorage.restore(),
|
|
294
|
+
sortStorage.restore(),
|
|
295
|
+
columnWidthsStorage.restore(),
|
|
285
296
|
]);
|
|
286
297
|
};
|
|
287
298
|
|
|
299
|
+
/* sequentially: every route write is a router.replace() on top of the current query */
|
|
300
|
+
const syncPersistence = async () => {
|
|
301
|
+
for (const controller of persistedStorageControllers) {
|
|
302
|
+
await controller.sync();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
288
306
|
const getHeaderByField = (field: string) => {
|
|
289
307
|
return headers.value.find((header) => header.field === field);
|
|
290
308
|
};
|
|
@@ -334,6 +352,7 @@ export const tableHeadersStoreBody = ({
|
|
|
334
352
|
columnReorder,
|
|
335
353
|
|
|
336
354
|
setupPersistence,
|
|
355
|
+
syncPersistence,
|
|
337
356
|
$reset,
|
|
338
357
|
};
|
|
339
358
|
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { createApp } from 'vue';
|
|
3
|
+
import { createMemoryHistory, createRouter, type Router } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
import { tablePaginationStoreBody } from '../createTablePaginationStore';
|
|
6
|
+
|
|
7
|
+
const routes = [
|
|
8
|
+
{
|
|
9
|
+
path: '/cases',
|
|
10
|
+
name: 'cases',
|
|
11
|
+
component: {
|
|
12
|
+
template: '<div />',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
describe('tablePaginationStoreBody', () => {
|
|
18
|
+
let router: Router;
|
|
19
|
+
let runWithRouter: <T>(fn: () => T) => T;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
router = createRouter({
|
|
23
|
+
history: createMemoryHistory(),
|
|
24
|
+
routes,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const app = createApp({});
|
|
28
|
+
app.use(router);
|
|
29
|
+
|
|
30
|
+
await router.push('/cases');
|
|
31
|
+
await router.isReady();
|
|
32
|
+
|
|
33
|
+
runWithRouter = (fn) => app.runWithContext(fn);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const setUpStore = async () => {
|
|
37
|
+
const store = tablePaginationStoreBody();
|
|
38
|
+
await runWithRouter(() => store.setupPersistence());
|
|
39
|
+
return store;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
it('starts on the first page with the default size', () => {
|
|
43
|
+
const store = tablePaginationStoreBody();
|
|
44
|
+
|
|
45
|
+
expect(store.page.value).toBe(1);
|
|
46
|
+
expect(store.size.value).toBe(10);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('updates page and size', () => {
|
|
50
|
+
const store = tablePaginationStoreBody();
|
|
51
|
+
|
|
52
|
+
store.updatePage(3);
|
|
53
|
+
store.updateSize(20);
|
|
54
|
+
|
|
55
|
+
expect(store.page.value).toBe(3);
|
|
56
|
+
expect(store.size.value).toBe(20);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('resets to the defaults', () => {
|
|
60
|
+
const store = tablePaginationStoreBody();
|
|
61
|
+
store.updatePage(3);
|
|
62
|
+
store.updateSize(20);
|
|
63
|
+
store.next.value = true;
|
|
64
|
+
|
|
65
|
+
store.$reset();
|
|
66
|
+
|
|
67
|
+
expect(store.page.value).toBe(1);
|
|
68
|
+
expect(store.size.value).toBe(10);
|
|
69
|
+
expect(store.next.value).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('persistence', () => {
|
|
73
|
+
it('restores page and size from the route query', async () => {
|
|
74
|
+
await router.push({
|
|
75
|
+
name: 'cases',
|
|
76
|
+
query: {
|
|
77
|
+
page: '3',
|
|
78
|
+
size: '20',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const store = await setUpStore();
|
|
83
|
+
|
|
84
|
+
expect(store.page.value).toBe(3);
|
|
85
|
+
expect(store.size.value).toBe(20);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('writes a changed page into the route query', async () => {
|
|
89
|
+
const store = await setUpStore();
|
|
90
|
+
|
|
91
|
+
store.updatePage(3);
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
93
|
+
|
|
94
|
+
expect(router.currentRoute.value.query.page).toBe('3');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('syncPersistence', () => {
|
|
99
|
+
it('publishes state the user changed', async () => {
|
|
100
|
+
const store = await setUpStore();
|
|
101
|
+
store.updatePage(3);
|
|
102
|
+
await router.push('/cases');
|
|
103
|
+
|
|
104
|
+
await runWithRouter(() => store.syncPersistence());
|
|
105
|
+
|
|
106
|
+
expect(router.currentRoute.value.query.page).toBe('3');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('publishes nothing while the state is still default', async () => {
|
|
110
|
+
const store = await setUpStore();
|
|
111
|
+
|
|
112
|
+
await runWithRouter(() => store.syncPersistence());
|
|
113
|
+
|
|
114
|
+
expect(router.currentRoute.value.query).toEqual({});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not overwrite a value already in the query', async () => {
|
|
118
|
+
const store = await setUpStore();
|
|
119
|
+
store.updatePage(3);
|
|
120
|
+
/* let the watcher settle, so its write cannot land after the push below */
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
122
|
+
await router.push({
|
|
123
|
+
name: 'cases',
|
|
124
|
+
query: {
|
|
125
|
+
page: '5',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await runWithRouter(() => store.syncPersistence());
|
|
130
|
+
|
|
131
|
+
expect(router.currentRoute.value.query.page).toBe('5');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ref } from 'vue';
|
|
2
2
|
|
|
3
3
|
import { createDatalistStore } from '../_shared/createDatalistStore';
|
|
4
|
+
import type { PersistedStorageController } from '../persist/PersistedStorage.types';
|
|
4
5
|
import { usePersistedStorage } from '../persist/usePersistedStorage';
|
|
5
6
|
import type { Identifiable } from '../types/createDatalistStore.types';
|
|
6
7
|
import type { useTableStoreConfig } from '../types/tableStore.types';
|
|
@@ -24,10 +25,18 @@ export const tablePaginationStoreBody = () => {
|
|
|
24
25
|
next.value = false;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
let persistedStorageControllers: PersistedStorageController[] = [];
|
|
29
|
+
|
|
27
30
|
const setupPersistence = () => {
|
|
28
|
-
const
|
|
31
|
+
const pageStorage = usePersistedStorage({
|
|
29
32
|
name: 'page',
|
|
30
33
|
value: page,
|
|
34
|
+
onStore: (save, { name }) => {
|
|
35
|
+
return save({
|
|
36
|
+
name,
|
|
37
|
+
value: `${page.value}`,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
31
40
|
onRestore: async (restore, name) => {
|
|
32
41
|
const value = await restore(name);
|
|
33
42
|
const numValue = Number(value);
|
|
@@ -35,9 +44,15 @@ export const tablePaginationStoreBody = () => {
|
|
|
35
44
|
},
|
|
36
45
|
});
|
|
37
46
|
|
|
38
|
-
const
|
|
47
|
+
const sizeStorage = usePersistedStorage({
|
|
39
48
|
name: 'size',
|
|
40
49
|
value: size,
|
|
50
|
+
onStore: (save, { name }) => {
|
|
51
|
+
return save({
|
|
52
|
+
name,
|
|
53
|
+
value: `${size.value}`,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
41
56
|
onRestore: async (restore, name) => {
|
|
42
57
|
const value = await restore(name);
|
|
43
58
|
const numValue = Number(value);
|
|
@@ -45,12 +60,24 @@ export const tablePaginationStoreBody = () => {
|
|
|
45
60
|
},
|
|
46
61
|
});
|
|
47
62
|
|
|
63
|
+
persistedStorageControllers = [
|
|
64
|
+
pageStorage,
|
|
65
|
+
sizeStorage,
|
|
66
|
+
];
|
|
67
|
+
|
|
48
68
|
return Promise.allSettled([
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
pageStorage.restore(),
|
|
70
|
+
sizeStorage.restore(),
|
|
51
71
|
]);
|
|
52
72
|
};
|
|
53
73
|
|
|
74
|
+
/* sequentially: every route write is a router.replace() on top of the current query */
|
|
75
|
+
const syncPersistence = async () => {
|
|
76
|
+
for (const controller of persistedStorageControllers) {
|
|
77
|
+
await controller.sync();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
54
81
|
return {
|
|
55
82
|
page,
|
|
56
83
|
size,
|
|
@@ -60,6 +87,7 @@ export const tablePaginationStoreBody = () => {
|
|
|
60
87
|
updateSize,
|
|
61
88
|
|
|
62
89
|
setupPersistence,
|
|
90
|
+
syncPersistence,
|
|
63
91
|
$reset,
|
|
64
92
|
};
|
|
65
93
|
};
|
|
@@ -22,6 +22,10 @@ export interface StorageLike {
|
|
|
22
22
|
removeItem(key: string): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface PersistedStorageAdapter extends StorageLike {
|
|
26
|
+
type: PersistedStorageType;
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
export interface PersistedPropertyConfig {
|
|
26
30
|
name: string;
|
|
27
31
|
// note: createTableHeadersStore passes a computed here, which the default
|
|
@@ -55,5 +59,15 @@ export interface PersistedStorageController {
|
|
|
55
59
|
watch: () => void;
|
|
56
60
|
unwatch: () => void;
|
|
57
61
|
restore: () => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Publishes the current value to every storage that holds nothing for `name`.
|
|
64
|
+
* Storages that already hold a value are left untouched, so an explicit route
|
|
65
|
+
* query always wins over in-memory state.
|
|
66
|
+
*
|
|
67
|
+
* A no-op for state still equal to the defaults: `restore()` captures the
|
|
68
|
+
* snapshot a freshly created store serializes, and a controller that never
|
|
69
|
+
* restored can only tell empty values apart.
|
|
70
|
+
*/
|
|
71
|
+
sync: () => Promise<void>;
|
|
58
72
|
reset: () => Promise<void>;
|
|
59
73
|
}
|