@webitel/ui-datalist 1.1.64 → 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/card/stores/createCardStore.ts +22 -11
- 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
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { isEmpty } from '@webitel/ui-sdk/scripts';
|
|
1
2
|
import { type WatchHandle, watch } from 'vue';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
type PersistableValue,
|
|
5
6
|
type PersistedPropertyConfig,
|
|
7
|
+
type PersistedStorageAdapter,
|
|
6
8
|
type PersistedStorageController,
|
|
7
9
|
PersistedStorageType,
|
|
8
|
-
type
|
|
10
|
+
type PersistStorableValue,
|
|
9
11
|
} from './PersistedStorage.types';
|
|
10
12
|
import { useLocalStoragePersistedStorage } from './useLocalStoragePersistedStorage';
|
|
11
13
|
import { useRoutePersistedStorage } from './useRoutePersistedStorage';
|
|
12
14
|
|
|
15
|
+
const toStorableValue = (value: PersistableValue): PersistStorableValue => {
|
|
16
|
+
return typeof value === 'string' ? value : (value?.toString() ?? '');
|
|
17
|
+
};
|
|
18
|
+
|
|
13
19
|
export const usePersistedStorage = ({
|
|
14
20
|
name,
|
|
15
21
|
value,
|
|
@@ -22,10 +28,9 @@ export const usePersistedStorage = ({
|
|
|
22
28
|
onRestore,
|
|
23
29
|
}: PersistedPropertyConfig): PersistedStorageController => {
|
|
24
30
|
let unwatch: WatchHandle | null = null;
|
|
31
|
+
let defaultSnapshot: PersistStorableValue | undefined;
|
|
25
32
|
|
|
26
|
-
const
|
|
27
|
-
const getItemFns: StorageLike['getItem'][] = [];
|
|
28
|
-
const removeItemFns: StorageLike['removeItem'][] = [];
|
|
33
|
+
const adapters: PersistedStorageAdapter[] = [];
|
|
29
34
|
|
|
30
35
|
// `null` entries are kept: callers below pick the first non-null value, so the
|
|
31
36
|
// per-storage misses have to stay in the list to preserve priority order.
|
|
@@ -33,7 +38,7 @@ export const usePersistedStorage = ({
|
|
|
33
38
|
name: string,
|
|
34
39
|
): Promise<Array<PersistableValue | null>> => {
|
|
35
40
|
const settledResults = await Promise.allSettled(
|
|
36
|
-
|
|
41
|
+
adapters.map(({ getItem }) => getItem(name)),
|
|
37
42
|
);
|
|
38
43
|
|
|
39
44
|
return settledResults.reduce(
|
|
@@ -57,58 +62,67 @@ export const usePersistedStorage = ({
|
|
|
57
62
|
order matters, as the first storage in the list has the highest priority
|
|
58
63
|
*/
|
|
59
64
|
if (storages.includes(PersistedStorageType.Route)) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
adapters.push({
|
|
66
|
+
type: PersistedStorageType.Route,
|
|
67
|
+
...useRoutePersistedStorage(),
|
|
68
|
+
});
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
if (storages.includes(PersistedStorageType.LocalStorage)) {
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
adapters.push({
|
|
73
|
+
type: PersistedStorageType.LocalStorage,
|
|
74
|
+
...useLocalStoragePersistedStorage({
|
|
75
|
+
storagePath,
|
|
76
|
+
}),
|
|
69
77
|
});
|
|
70
|
-
setItemFns.push(setItem);
|
|
71
|
-
getItemFns.push(getItem);
|
|
72
|
-
removeItemFns.push(removeItem);
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
/*
|
|
81
|
+
runs the storing path with a `save` doing whatever the caller needs:
|
|
82
|
+
writing to the storages, or merely capturing the serialized value
|
|
83
|
+
*/
|
|
84
|
+
const runStoringPath = async (
|
|
85
|
+
save: (params: { name: string; value: PersistableValue }) => Promise<void>,
|
|
86
|
+
) => {
|
|
87
|
+
if (onStore) {
|
|
88
|
+
/* save is wrapped in one callback, so that onStore is called only once */
|
|
89
|
+
return onStore(save, {
|
|
90
|
+
name,
|
|
91
|
+
value: value.value ?? '',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return save({
|
|
96
|
+
name,
|
|
97
|
+
value: value.value ?? '',
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const serialize = async () => {
|
|
102
|
+
let serializedValue: PersistStorableValue = '';
|
|
103
|
+
|
|
104
|
+
await runStoringPath(async ({ value: storedValue }) => {
|
|
105
|
+
serializedValue = toStorableValue(storedValue);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return serializedValue;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const store = (targets: PersistedStorageAdapter[]) => {
|
|
112
|
+
if (!targets.length) return Promise.resolve();
|
|
113
|
+
|
|
114
|
+
return runStoringPath(async ({ name, value: storedValue }) => {
|
|
115
|
+
await Promise.all(
|
|
116
|
+
targets.map((adapter) => adapter.setItem(name, storedValue)),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
75
121
|
const startWatch = () => {
|
|
76
122
|
unwatch = watch(
|
|
77
123
|
value,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if onStore callback is provided,
|
|
81
|
-
call custom logic for storing value
|
|
82
|
-
*/
|
|
83
|
-
if (onStore) {
|
|
84
|
-
/*
|
|
85
|
-
wrap all setItemFns in one callback
|
|
86
|
-
so that onStore is called only once on each value change
|
|
87
|
-
*/
|
|
88
|
-
const save = async ({
|
|
89
|
-
name,
|
|
90
|
-
value: storedValue,
|
|
91
|
-
}: {
|
|
92
|
-
name: string;
|
|
93
|
-
value: PersistableValue;
|
|
94
|
-
}) => {
|
|
95
|
-
setItemFns.forEach((setter) => {
|
|
96
|
-
setter(name, storedValue);
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
await onStore(save, {
|
|
100
|
-
name,
|
|
101
|
-
value: value.value ?? '',
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
/*
|
|
105
|
-
else, perform default storing logic
|
|
106
|
-
*/
|
|
107
|
-
const storedValue = value.value ?? '';
|
|
108
|
-
setItemFns.forEach((setter) => {
|
|
109
|
-
setter(name, storedValue);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
124
|
+
() => {
|
|
125
|
+
store(adapters);
|
|
112
126
|
},
|
|
113
127
|
{
|
|
114
128
|
deep: true,
|
|
@@ -118,14 +132,13 @@ export const usePersistedStorage = ({
|
|
|
118
132
|
|
|
119
133
|
const restore = async () => {
|
|
120
134
|
/*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
nothing has mutated the value yet, so this is what a freshly created store
|
|
136
|
+
serializes – sync() uses it to tell untouched state from a real one
|
|
137
|
+
*/
|
|
138
|
+
defaultSnapshot = await serialize();
|
|
139
|
+
|
|
124
140
|
if (onRestore) {
|
|
125
|
-
/*
|
|
126
|
-
wrap all getItemFns in one callback
|
|
127
|
-
so that onRestore is called only once on each value change
|
|
128
|
-
*/
|
|
141
|
+
/* every getter is wrapped in one callback, so that onRestore is called only once */
|
|
129
142
|
const restore = async (name: string) => {
|
|
130
143
|
const restoredValues = await composedValueGetter(name);
|
|
131
144
|
/*
|
|
@@ -138,13 +151,15 @@ export const usePersistedStorage = ({
|
|
|
138
151
|
};
|
|
139
152
|
await onRestore(restore, name);
|
|
140
153
|
} else {
|
|
154
|
+
const restoredValues = await composedValueGetter(name);
|
|
141
155
|
/*
|
|
142
|
-
|
|
156
|
+
loose check on purpose: useRoutePersistedStorage resolves `undefined`
|
|
157
|
+
for a missing query param, so a storage listed after the route one
|
|
158
|
+
would never win with a strict `!== null`
|
|
143
159
|
*/
|
|
144
|
-
const
|
|
145
|
-
const restoredValue = restoredValues.find((value) => value !== null);
|
|
160
|
+
const restoredValue = restoredValues.find((value) => value != null);
|
|
146
161
|
|
|
147
|
-
if (restoredValue
|
|
162
|
+
if (restoredValue != null) {
|
|
148
163
|
value.value = restoredValue;
|
|
149
164
|
}
|
|
150
165
|
}
|
|
@@ -157,8 +172,25 @@ export const usePersistedStorage = ({
|
|
|
157
172
|
}
|
|
158
173
|
};
|
|
159
174
|
|
|
175
|
+
const sync = async () => {
|
|
176
|
+
const serializedValue = await serialize();
|
|
177
|
+
|
|
178
|
+
if (isEmpty(serializedValue) || serializedValue === defaultSnapshot) return;
|
|
179
|
+
|
|
180
|
+
const settledResults = await Promise.allSettled(
|
|
181
|
+
adapters.map(({ getItem }) => getItem(name)),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const emptyStorages = adapters.filter((_, index) => {
|
|
185
|
+
const result = settledResults[index];
|
|
186
|
+
return result.status === 'fulfilled' && result.value == null;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return store(emptyStorages);
|
|
190
|
+
};
|
|
191
|
+
|
|
160
192
|
const reset = async () => {
|
|
161
|
-
await Promise.all(
|
|
193
|
+
await Promise.all(adapters.map(({ removeItem }) => removeItem(name)));
|
|
162
194
|
};
|
|
163
195
|
|
|
164
196
|
const endWatch = () => unwatch?.();
|
|
@@ -167,6 +199,7 @@ export const usePersistedStorage = ({
|
|
|
167
199
|
watch: startWatch,
|
|
168
200
|
unwatch: endWatch,
|
|
169
201
|
restore,
|
|
202
|
+
sync,
|
|
170
203
|
reset,
|
|
171
204
|
};
|
|
172
205
|
};
|
|
@@ -2,6 +2,23 @@ import { useRoute, useRouter } from 'vue-router';
|
|
|
2
2
|
|
|
3
3
|
import type { StorageLike } from './PersistedStorage.types.ts';
|
|
4
4
|
|
|
5
|
+
/*
|
|
6
|
+
every write is a router.replace() built on top of the current query, and the
|
|
7
|
+
query only changes once the navigation resolves – so concurrent writes would
|
|
8
|
+
all start from the same snapshot and drop each other's keys. they are queued
|
|
9
|
+
process-wide: a table store writes several properties at once (a new sort
|
|
10
|
+
makes both `sort` and `fields` emit), and all of them target the same router
|
|
11
|
+
*/
|
|
12
|
+
let pendingWrite: Promise<unknown> = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
const enqueueWrite = <T>(write: () => Promise<T>): Promise<T> => {
|
|
15
|
+
const result = pendingWrite.then(write, write);
|
|
16
|
+
|
|
17
|
+
pendingWrite = result.catch(() => {});
|
|
18
|
+
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
|
|
5
22
|
export const useRoutePersistedStorage = (): StorageLike => {
|
|
6
23
|
const router = useRouter();
|
|
7
24
|
const route = useRoute();
|
|
@@ -11,27 +28,32 @@ export const useRoutePersistedStorage = (): StorageLike => {
|
|
|
11
28
|
};
|
|
12
29
|
|
|
13
30
|
const setItem = async (key: string, value: string | string[]) => {
|
|
14
|
-
await
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
await enqueueWrite(() =>
|
|
32
|
+
router.replace({
|
|
33
|
+
name: route.name,
|
|
34
|
+
params: route.params,
|
|
35
|
+
hash: route.hash,
|
|
36
|
+
query: {
|
|
37
|
+
...route.query,
|
|
38
|
+
[key]: value,
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
23
42
|
};
|
|
24
43
|
|
|
25
44
|
const removeItem = async (key: string) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
await enqueueWrite(() => {
|
|
46
|
+
const query = {
|
|
47
|
+
...route.query,
|
|
48
|
+
};
|
|
49
|
+
delete query[key];
|
|
50
|
+
|
|
51
|
+
return router.replace({
|
|
52
|
+
name: route.name,
|
|
53
|
+
params: route.params,
|
|
54
|
+
hash: route.hash,
|
|
55
|
+
query,
|
|
56
|
+
});
|
|
35
57
|
});
|
|
36
58
|
};
|
|
37
59
|
|
|
@@ -46,6 +46,7 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
46
46
|
// $reset: $resetPaginationStore,
|
|
47
47
|
$patch: $patchPaginationStore,
|
|
48
48
|
setupPersistence: setupPaginationPersistence,
|
|
49
|
+
syncPersistence: syncPaginationPersistence,
|
|
49
50
|
} = paginationStore;
|
|
50
51
|
|
|
51
52
|
const headersStore = useHeadersStore();
|
|
@@ -63,6 +64,7 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
63
64
|
columnReorder,
|
|
64
65
|
updateShownHeaders,
|
|
65
66
|
setupPersistence: setupHeadersPersistence,
|
|
67
|
+
syncPersistence: syncHeadersPersistence,
|
|
66
68
|
} = headersStore;
|
|
67
69
|
|
|
68
70
|
const filtersStore = useFiltersStore();
|
|
@@ -77,6 +79,7 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
77
79
|
updateFilter,
|
|
78
80
|
deleteFilter,
|
|
79
81
|
setupPersistence: setupFiltersPersistence,
|
|
82
|
+
syncPersistence: syncFiltersPersistence,
|
|
80
83
|
updateSearchMode,
|
|
81
84
|
} = filtersStore;
|
|
82
85
|
|
|
@@ -285,6 +288,25 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
285
288
|
isStoreSetUp.value = true;
|
|
286
289
|
};
|
|
287
290
|
|
|
291
|
+
/**
|
|
292
|
+
* @description
|
|
293
|
+
* Republishes the current store state into the storages that hold nothing
|
|
294
|
+
* for it – in practice, into the route query.
|
|
295
|
+
*
|
|
296
|
+
* Persistence is restored only once per app lifetime (see `setupStore`),
|
|
297
|
+
* and afterwards the query is written by watchers only, i.e. on change.
|
|
298
|
+
* So a registry re-opened with state still in memory (closing a card back to
|
|
299
|
+
* it, or reaching it from the menu) used to render filtered data behind a
|
|
300
|
+
* bare url, losing filters on reload or on copying the link.
|
|
301
|
+
*
|
|
302
|
+
* [WTEL-10093](https://webitel.atlassian.net/browse/WTEL-10093)
|
|
303
|
+
*/
|
|
304
|
+
const syncPersistence = async () => {
|
|
305
|
+
await syncPaginationPersistence();
|
|
306
|
+
await syncFiltersPersistence();
|
|
307
|
+
await syncHeadersPersistence();
|
|
308
|
+
};
|
|
309
|
+
|
|
288
310
|
const initialize = async ({
|
|
289
311
|
parentId: storeParentId,
|
|
290
312
|
}: {
|
|
@@ -294,8 +316,22 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
294
316
|
parentId.value = storeParentId;
|
|
295
317
|
}
|
|
296
318
|
|
|
319
|
+
const isStoreAlreadySetUp = isStoreSetUp.value;
|
|
320
|
+
|
|
297
321
|
await setupStore();
|
|
298
322
|
|
|
323
|
+
/*
|
|
324
|
+
on the first setup the restore path is authoritative.
|
|
325
|
+
|
|
326
|
+
a store initialized with a parentId is a list nested in a card page, not a
|
|
327
|
+
registry: it shares the query param names with the registry stores, and
|
|
328
|
+
the url it would publish into is the card one – so it keeps writing on
|
|
329
|
+
change only, and syncs merely on demand
|
|
330
|
+
*/
|
|
331
|
+
if (isStoreAlreadySetUp && !disablePersistence && !storeParentId) {
|
|
332
|
+
await syncPersistence();
|
|
333
|
+
}
|
|
334
|
+
|
|
299
335
|
return loadDataList();
|
|
300
336
|
};
|
|
301
337
|
|
|
@@ -329,6 +365,7 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
329
365
|
|
|
330
366
|
setupStore, // only setup, no data loading
|
|
331
367
|
initialize, // setup + load data
|
|
368
|
+
syncPersistence, // republish store state into the route query
|
|
332
369
|
|
|
333
370
|
loadDataList,
|
|
334
371
|
appendToDataList,
|