@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/ui-datalist",
3
- "version": "1.1.77",
3
+ "version": "1.1.79",
4
4
  "description": "Toolkit for building data lists in webitel ui system",
5
5
  "scripts": {
6
6
  "build:types": "vue-tsc -b tsconfig.build.json",
@@ -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 { effectScope, ref, toRaw, watch } from 'vue';
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
- filters: JSON.stringify({
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.filters).toContain('open');
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.filters).toContain('open');
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
- () => dynamicFilterAddAction?.value?.hidePopover(),
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
- fields: 'createdAt',
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
- fields: 'agent,createdAt',
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
- fields: 'member',
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.fields).toBe('name');
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]: () => useRoutePersistedStorage(),
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
- export const useRoutePersistedStorage = (): StorageLike => {
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,