@webitel/ui-datalist 1.1.77 → 1.1.79
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 +4 -3
- package/src/modules/filters/__tests__/createTableFiltersStore.spec.ts +9 -3
- package/src/modules/filters/components/dynamic-filter-add-action.vue +43 -2
- package/src/modules/headers/__tests__/createTableHeadersStore.spec.ts +6 -4
- package/src/modules/persist/usePersistedStorage.ts +4 -1
- package/src/modules/persist/useRoutePersistedStorage.ts +11 -4
- package/src/modules/table/createTableStore.store.ts +11 -1
- package/types/.tsbuildinfo +1 -1
- package/types/modules/card/stores/createCardStore.d.ts +2 -1
- package/types/modules/persist/useRoutePersistedStorage.d.ts +3 -1
package/package.json
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
import { getDefaultsFromZodSchema } from '@webitel/api-services/utils';
|
|
7
7
|
import type { ApiModule } from '@webitel/ui-sdk/src/api/types/ApiModule';
|
|
8
8
|
import { defineStore } from 'pinia';
|
|
9
|
-
import {
|
|
9
|
+
import type { MaybeRef } from 'vue';
|
|
10
|
+
import { effectScope, ref, toRaw, toValue, watch } from 'vue';
|
|
10
11
|
import type { z } from 'zod/v4';
|
|
11
12
|
|
|
12
13
|
import type { CardItemId, CardParentId } from '../types/CardStore.types';
|
|
@@ -32,7 +33,7 @@ export const createCardStore = <
|
|
|
32
33
|
validationSchemaOptions,
|
|
33
34
|
}: {
|
|
34
35
|
namespace: string;
|
|
35
|
-
standardValidationSchema: z.ZodType
|
|
36
|
+
standardValidationSchema: MaybeRef<z.ZodType>;
|
|
36
37
|
/**
|
|
37
38
|
* Every method on {@link ApiModule} is optional, but a card store reads and
|
|
38
39
|
* writes one item, so these three are required here.
|
|
@@ -158,7 +159,7 @@ export const createCardStore = <
|
|
|
158
159
|
await loadItem();
|
|
159
160
|
} else if (standardValidationSchema) {
|
|
160
161
|
draftItemInstance.value = await getDefaultsFromZodSchema(
|
|
161
|
-
standardValidationSchema,
|
|
162
|
+
toValue(standardValidationSchema),
|
|
162
163
|
draftItemInstance.value,
|
|
163
164
|
);
|
|
164
165
|
} else {
|
|
@@ -15,6 +15,8 @@ const routes = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
const namespace = 'cases';
|
|
18
|
+
/* the `filters` route query key is namespaced by the store's storagePath */
|
|
19
|
+
const filtersQueryKey = `${namespace}/filters`;
|
|
18
20
|
|
|
19
21
|
describe('tableFiltersStoreBody', () => {
|
|
20
22
|
let router: Router;
|
|
@@ -84,7 +86,7 @@ describe('tableFiltersStoreBody', () => {
|
|
|
84
86
|
await router.push({
|
|
85
87
|
name: 'cases',
|
|
86
88
|
query: {
|
|
87
|
-
|
|
89
|
+
[filtersQueryKey]: JSON.stringify({
|
|
88
90
|
status_val: 'open',
|
|
89
91
|
}),
|
|
90
92
|
},
|
|
@@ -104,7 +106,9 @@ describe('tableFiltersStoreBody', () => {
|
|
|
104
106
|
});
|
|
105
107
|
await new Promise((resolve) => setTimeout(resolve));
|
|
106
108
|
|
|
107
|
-
expect(router.currentRoute.value.query
|
|
109
|
+
expect(router.currentRoute.value.query[filtersQueryKey]).toContain(
|
|
110
|
+
'open',
|
|
111
|
+
);
|
|
108
112
|
});
|
|
109
113
|
|
|
110
114
|
it('stores filters under the store namespace, not a shared key', async () => {
|
|
@@ -143,7 +147,9 @@ describe('tableFiltersStoreBody', () => {
|
|
|
143
147
|
|
|
144
148
|
await runWithRouter(() => store.syncPersistence());
|
|
145
149
|
|
|
146
|
-
expect(router.currentRoute.value.query
|
|
150
|
+
expect(router.currentRoute.value.query[filtersQueryKey]).toContain(
|
|
151
|
+
'open',
|
|
152
|
+
);
|
|
147
153
|
});
|
|
148
154
|
|
|
149
155
|
it('publishes nothing while no filter is applied', async () => {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
</template>
|
|
31
31
|
|
|
32
32
|
<script lang="ts" setup>
|
|
33
|
-
import { onClickOutside } from '@vueuse/core';
|
|
33
|
+
import { onClickOutside, useEventListener } from '@vueuse/core';
|
|
34
34
|
import { WtIconAction } from '@webitel/ui-sdk/components';
|
|
35
35
|
import { ref } from 'vue';
|
|
36
36
|
import { useI18n } from 'vue-i18n';
|
|
@@ -72,6 +72,43 @@ const dynamicFilterAddAction = ref<{
|
|
|
72
72
|
hidePopover: () => void;
|
|
73
73
|
} | null>(null);
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @author @HlukhovYe
|
|
77
|
+
*
|
|
78
|
+
* https://webitel.atlassian.net/browse/WTEL-10240
|
|
79
|
+
*
|
|
80
|
+
* A click fully outside the popover can also be the same click that closes a
|
|
81
|
+
* child select/multiselect/datepicker overlay (its own panel is teleported to
|
|
82
|
+
* `body`, so it's not a descendant of `popoverContentRef`, and the `ignore`
|
|
83
|
+
* list below only matches clicks landing directly on it, not this outside
|
|
84
|
+
* click). The browser blurs the overlay's focused element (e.g. a multiselect
|
|
85
|
+
* filter input) as part of the same mousedown that starts this click, so by
|
|
86
|
+
* the time onClickOutside's own `click`-phase handler runs, `activeElement`
|
|
87
|
+
* has already reset to `body` — too late to check. A capture-phase
|
|
88
|
+
* `pointerdown` listener runs earlier, while focus is still intact, so it's
|
|
89
|
+
* used here to snapshot whether an open field overlay currently has focus.
|
|
90
|
+
* If so, let that overlay's own outside-click handler close it and leave this
|
|
91
|
+
* popover open — otherwise both close on the same click.
|
|
92
|
+
*/
|
|
93
|
+
let hadOwnOpenFieldOverlayOnPointerdown = false;
|
|
94
|
+
useEventListener(
|
|
95
|
+
document,
|
|
96
|
+
'pointerdown',
|
|
97
|
+
() => {
|
|
98
|
+
const openFieldOverlay = document.querySelector(
|
|
99
|
+
'.p-select-overlay, .p-multiselect-overlay, .p-datepicker-panel',
|
|
100
|
+
);
|
|
101
|
+
hadOwnOpenFieldOverlayOnPointerdown = !!(
|
|
102
|
+
openFieldOverlay &&
|
|
103
|
+
(popoverContentRef.value?.contains(document.activeElement) ||
|
|
104
|
+
openFieldOverlay.contains(document.activeElement))
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
capture: true,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
75
112
|
/**
|
|
76
113
|
* @author @Oleksandr Palonnyi
|
|
77
114
|
*
|
|
@@ -84,7 +121,11 @@ const dynamicFilterAddAction = ref<{
|
|
|
84
121
|
* */
|
|
85
122
|
onClickOutside(
|
|
86
123
|
popoverContentRef,
|
|
87
|
-
() =>
|
|
124
|
+
() => {
|
|
125
|
+
if (hadOwnOpenFieldOverlayOnPointerdown) return;
|
|
126
|
+
|
|
127
|
+
dynamicFilterAddAction?.value?.hidePopover();
|
|
128
|
+
},
|
|
88
129
|
{
|
|
89
130
|
capture: true, // Fix for PrimeVue stopPropagation bug
|
|
90
131
|
ignore: [
|
|
@@ -16,6 +16,8 @@ const routes = [
|
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
const id = 'cases/headers';
|
|
19
|
+
/* the `fields` route query key is namespaced by the table's storagePath */
|
|
20
|
+
const fieldsQueryKey = `${id}/fields`;
|
|
19
21
|
|
|
20
22
|
const rawHeaders = [
|
|
21
23
|
{
|
|
@@ -148,7 +150,7 @@ describe('tableHeadersStoreBody', () => {
|
|
|
148
150
|
await router.push({
|
|
149
151
|
name: 'cases',
|
|
150
152
|
query: {
|
|
151
|
-
|
|
153
|
+
[fieldsQueryKey]: 'createdAt',
|
|
152
154
|
},
|
|
153
155
|
});
|
|
154
156
|
|
|
@@ -237,7 +239,7 @@ describe('tableHeadersStoreBody', () => {
|
|
|
237
239
|
await router.push({
|
|
238
240
|
name: 'cases',
|
|
239
241
|
query: {
|
|
240
|
-
|
|
242
|
+
[fieldsQueryKey]: 'agent,createdAt',
|
|
241
243
|
},
|
|
242
244
|
});
|
|
243
245
|
|
|
@@ -280,7 +282,7 @@ describe('tableHeadersStoreBody', () => {
|
|
|
280
282
|
await router.push({
|
|
281
283
|
name: 'cases',
|
|
282
284
|
query: {
|
|
283
|
-
|
|
285
|
+
[fieldsQueryKey]: 'member',
|
|
284
286
|
},
|
|
285
287
|
});
|
|
286
288
|
|
|
@@ -326,7 +328,7 @@ describe('tableHeadersStoreBody', () => {
|
|
|
326
328
|
|
|
327
329
|
await runWithRouter(() => store.syncPersistence());
|
|
328
330
|
|
|
329
|
-
expect(router.currentRoute.value.query
|
|
331
|
+
expect(router.currentRoute.value.query[fieldsQueryKey]).toBe('name');
|
|
330
332
|
});
|
|
331
333
|
|
|
332
334
|
it('publishes nothing while the columns are still default', async () => {
|
|
@@ -62,7 +62,10 @@ export const usePersistedStorage = ({
|
|
|
62
62
|
|
|
63
63
|
/* keyed by the enum, so a kind without an adapter is a type error */
|
|
64
64
|
const adapterFactories: Record<PersistedStorageType, () => StorageLike> = {
|
|
65
|
-
[PersistedStorageType.Route]: () =>
|
|
65
|
+
[PersistedStorageType.Route]: () =>
|
|
66
|
+
useRoutePersistedStorage({
|
|
67
|
+
storagePath,
|
|
68
|
+
}),
|
|
66
69
|
[PersistedStorageType.LocalStorage]: () =>
|
|
67
70
|
useLocalStoragePersistedStorage({
|
|
68
71
|
storagePath,
|
|
@@ -19,12 +19,19 @@ const enqueueWrite = <T>(write: () => Promise<T>): Promise<T> => {
|
|
|
19
19
|
return result;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const makeQueryKey = (storagePath: string, key: string) =>
|
|
23
|
+
storagePath ? `${storagePath}/${key}` : key;
|
|
24
|
+
|
|
25
|
+
export const useRoutePersistedStorage = ({
|
|
26
|
+
storagePath = '',
|
|
27
|
+
}: {
|
|
28
|
+
storagePath?: string;
|
|
29
|
+
} = {}): StorageLike => {
|
|
23
30
|
const router = useRouter();
|
|
24
31
|
const route = useRoute();
|
|
25
32
|
|
|
26
33
|
const getItem = async (key: string) => {
|
|
27
|
-
return route.query[key];
|
|
34
|
+
return route.query[makeQueryKey(storagePath, key)];
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
const setItem = async (key: string, value: string | string[]) => {
|
|
@@ -35,7 +42,7 @@ export const useRoutePersistedStorage = (): StorageLike => {
|
|
|
35
42
|
hash: route.hash,
|
|
36
43
|
query: {
|
|
37
44
|
...route.query,
|
|
38
|
-
[key]: value,
|
|
45
|
+
[makeQueryKey(storagePath, key)]: value,
|
|
39
46
|
},
|
|
40
47
|
}),
|
|
41
48
|
);
|
|
@@ -46,7 +53,7 @@ export const useRoutePersistedStorage = (): StorageLike => {
|
|
|
46
53
|
const query = {
|
|
47
54
|
...route.query,
|
|
48
55
|
};
|
|
49
|
-
delete query[key];
|
|
56
|
+
delete query[makeQueryKey(storagePath, key)];
|
|
50
57
|
|
|
51
58
|
return router.replace({
|
|
52
59
|
name: route.name,
|
|
@@ -103,9 +103,19 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
103
103
|
selected.value = value;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
// filtersManager is reactive(), so its values come back as reactive
|
|
107
|
+
// proxies — strip that here so apiModule implementations never have to.
|
|
108
|
+
const toRawFilterValues = (source: Record<string, unknown>) =>
|
|
109
|
+
Object.fromEntries(
|
|
110
|
+
Object.entries(source).map(([key, value]) => [
|
|
111
|
+
key,
|
|
112
|
+
toRaw(value),
|
|
113
|
+
]),
|
|
114
|
+
);
|
|
115
|
+
|
|
106
116
|
// Always request `id` (Vuex REQUIRED_FIELDS default) — needed for select/delete/open
|
|
107
117
|
const getLoadDataParams = () => ({
|
|
108
|
-
...filtersManager.value.getAllValues(),
|
|
118
|
+
...toRawFilterValues(filtersManager.value.getAllValues()),
|
|
109
119
|
page: page.value,
|
|
110
120
|
size: size.value,
|
|
111
121
|
sort: sort.value,
|