@webitel/ui-datalist 1.1.66 → 1.1.68

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.66",
3
+ "version": "1.1.68",
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",
@@ -3,7 +3,7 @@
3
3
  :label="labelValue"
4
4
  :search-method="props.filterConfig.searchRecords"
5
5
  :v="!disableValidation && vList"
6
- :model-value="model?.list"
6
+ :model-value="value.list"
7
7
  data-key="id"
8
8
  option-value="id"
9
9
  v-bind="$attrs"
@@ -12,9 +12,9 @@
12
12
  <wt-checkbox
13
13
  v-if="!props.filterConfig?.hideUnassigned"
14
14
  :label="t('reusable.showUnassigned')"
15
- :selected="!!model?.unassigned"
15
+ :selected="!!value.unassigned"
16
16
  :v="!disableValidation && vUnassigned"
17
- @update:selected="model.unassigned = !!$event"
17
+ @update:selected="handleInput('unassigned', !!$event)"
18
18
  />
19
19
  </template>
20
20
 
@@ -27,12 +27,6 @@ import { useI18n } from 'vue-i18n';
27
27
 
28
28
  import { IContactGroupFilterConfig } from './index';
29
29
 
30
- const props = defineProps<{
31
- filterConfig: IContactGroupFilterConfig;
32
- disableValidation?: boolean;
33
- hideLabel?: boolean;
34
- }>();
35
-
36
30
  type ModelValue = {
37
31
  list?: string[];
38
32
  unassigned?: boolean | null;
@@ -45,39 +39,62 @@ const model = defineModel<ModelValue>({
45
39
  }),
46
40
  });
47
41
 
42
+ const value = computed<ModelValue>(
43
+ () =>
44
+ model.value ?? {
45
+ list: [],
46
+ unassigned: null,
47
+ },
48
+ );
49
+
50
+ const handleInput = <K extends keyof ModelValue>(
51
+ key: K,
52
+ newFieldValue: ModelValue[K],
53
+ ) => {
54
+ model.value = {
55
+ ...value.value,
56
+ [key]: newFieldValue,
57
+ };
58
+ };
59
+
60
+ const changeListValue = (event: string[]) => {
61
+ if (!event.length && !value.value.unassigned) {
62
+ model.value = {};
63
+ return;
64
+ }
65
+ handleInput('list', event);
66
+ };
67
+
68
+ const props = defineProps<{
69
+ filterConfig: IContactGroupFilterConfig;
70
+ disableValidation?: boolean;
71
+ hideLabel?: boolean;
72
+ }>();
73
+
48
74
  const emit = defineEmits<{
49
75
  'update:invalid': [
50
76
  boolean,
51
77
  ];
52
78
  }>();
79
+
53
80
  const { t } = useI18n();
54
81
 
55
82
  const labelValue = computed(() =>
56
83
  props?.hideLabel ? undefined : t('webitelUI.filters.filterValue'),
57
84
  );
58
85
 
59
- const changeListValue = (event: string[]) => {
60
- if (!event.length && !model.value.unassigned) {
61
- model.value = {};
62
- return;
63
- }
64
- model.value = {
65
- ...model.value,
66
- list: event,
67
- };
68
- };
69
86
  const v$ = useVuelidate<{
70
87
  model: ModelValue;
71
88
  }>(
72
89
  computed(() => ({
73
90
  model: {
74
91
  list: {
75
- required: requiredIf(() => !model.value.unassigned),
92
+ required: requiredIf(() => !value.value.unassigned),
76
93
  },
77
94
  unassigned: {
78
95
  required: requiredIf(
79
96
  () =>
80
- !!props.filterConfig?.hideUnassigned && !model.value.list?.length,
97
+ !!props.filterConfig?.hideUnassigned && !value.value.list?.length,
81
98
  ),
82
99
  },
83
100
  },
@@ -106,11 +123,9 @@ onMounted(() => {
106
123
  });
107
124
 
108
125
  watch(
109
- () => v$?.value?.$invalid,
126
+ () => v$.value.$invalid,
110
127
  (invalid) => {
111
- if (v$?.value) {
112
- emit('update:invalid', invalid);
113
- }
128
+ emit('update:invalid', invalid);
114
129
  },
115
130
  {
116
131
  immediate: true,
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest';
2
- import { createApp } from 'vue';
2
+ import { createApp, ref } from 'vue';
3
3
  import { createMemoryHistory, createRouter, type Router } from 'vue-router';
4
4
 
5
5
  import type { DatalistTableHeader } from '../../types/tableStore.types';
@@ -141,6 +141,149 @@ describe('tableHeadersStoreBody', () => {
141
141
  });
142
142
  });
143
143
 
144
+ describe('access', () => {
145
+ const createGatedStore = (gated: DatalistTableHeader[]) =>
146
+ tableHeadersStoreBody({
147
+ rawHeaders: [
148
+ ...rawHeaders.map((header) => ({
149
+ ...header,
150
+ })),
151
+ ...gated,
152
+ ],
153
+ id,
154
+ });
155
+
156
+ it('drops a denied header from headers, shownHeaders and fields', () => {
157
+ const store = createGatedStore([
158
+ {
159
+ field: 'agent',
160
+ show: true,
161
+ sort: null,
162
+ access: () => false,
163
+ } as DatalistTableHeader,
164
+ ]);
165
+
166
+ expect(store.headers.value.map(({ field }) => field)).not.toContain(
167
+ 'agent',
168
+ );
169
+ expect(store.shownHeaders.value.map(({ field }) => field)).not.toContain(
170
+ 'agent',
171
+ );
172
+ expect(store.fields.value).not.toContain('agent');
173
+ });
174
+
175
+ it('keeps a granted header and leaves ungated headers untouched', () => {
176
+ const store = createGatedStore([
177
+ {
178
+ field: 'agent',
179
+ show: true,
180
+ sort: null,
181
+ access: () => true,
182
+ } as DatalistTableHeader,
183
+ ]);
184
+
185
+ expect(store.fields.value).toEqual([
186
+ 'name',
187
+ 'subject',
188
+ 'agent',
189
+ ]);
190
+ });
191
+
192
+ it('unwraps a ref returned by the access gate', () => {
193
+ const store = createGatedStore([
194
+ {
195
+ field: 'agent',
196
+ show: true,
197
+ sort: null,
198
+ access: () => ref(false),
199
+ } as DatalistTableHeader,
200
+ ]);
201
+
202
+ expect(store.fields.value).toEqual([
203
+ 'name',
204
+ 'subject',
205
+ ]);
206
+ });
207
+
208
+ it('does not resurrect a denied header from the persisted fields', async () => {
209
+ await router.push({
210
+ name: 'cases',
211
+ query: {
212
+ fields: 'agent,createdAt',
213
+ },
214
+ });
215
+
216
+ const store = createGatedStore([
217
+ {
218
+ field: 'agent',
219
+ show: true,
220
+ sort: null,
221
+ access: () => false,
222
+ } as DatalistTableHeader,
223
+ ]);
224
+ await runWithRouter(() => store.setupPersistence());
225
+
226
+ expect(store.fields.value).toEqual([
227
+ 'createdAt',
228
+ ]);
229
+ expect(store.headers.value.map(({ field }) => field)).not.toContain(
230
+ 'agent',
231
+ );
232
+ });
233
+
234
+ it('does not resurrect a denied header on reset', () => {
235
+ const store = createGatedStore([
236
+ {
237
+ field: 'agent',
238
+ show: true,
239
+ sort: null,
240
+ access: () => false,
241
+ } as DatalistTableHeader,
242
+ ]);
243
+
244
+ store.$reset();
245
+
246
+ expect(store.headers.value.map(({ field }) => field)).not.toContain(
247
+ 'agent',
248
+ );
249
+ });
250
+
251
+ it('keeps a field alive while another allowed header still claims it', async () => {
252
+ await router.push({
253
+ name: 'cases',
254
+ query: {
255
+ fields: 'member',
256
+ },
257
+ });
258
+
259
+ /* `member` and `memberId` share one API field — only one is gated */
260
+ const store = createGatedStore([
261
+ {
262
+ value: 'member',
263
+ field: 'member',
264
+ show: true,
265
+ sort: null,
266
+ } as DatalistTableHeader,
267
+ {
268
+ value: 'memberId',
269
+ field: 'member',
270
+ show: false,
271
+ sort: null,
272
+ access: () => false,
273
+ } as DatalistTableHeader,
274
+ ]);
275
+ await runWithRouter(() => store.setupPersistence());
276
+
277
+ expect(store.headers.value.map(({ value }) => value)).toContain('member');
278
+ expect(store.headers.value.map(({ value }) => value)).not.toContain(
279
+ 'memberId',
280
+ );
281
+ expect(store.fields.value).toEqual([
282
+ 'member',
283
+ ]);
284
+ });
285
+ });
286
+
144
287
  describe('syncPersistence', () => {
145
288
  it('publishes a customized column set', async () => {
146
289
  const store = await setUpStore();
@@ -1,7 +1,7 @@
1
1
  import type { WtTableSortOrder } from '@webitel/ui-sdk/components/wt-table/types/WtTable';
2
2
  import { sortToQueryAdapter } from '@webitel/ui-sdk/scripts';
3
3
  import { SortSymbols } from '@webitel/ui-sdk/scripts/sortQueryAdapters';
4
- import { computed, nextTick, ref } from 'vue';
4
+ import { computed, nextTick, ref, unref } from 'vue';
5
5
 
6
6
  import { createDatalistStore } from '../_shared/createDatalistStore';
7
7
  import {
@@ -24,7 +24,21 @@ export const tableHeadersStoreBody = ({
24
24
  rawHeaders,
25
25
  id,
26
26
  }: TableHeadersStoreBodyParams) => {
27
- const headers = ref<DatalistTableHeader[]>(rawHeaders);
27
+ const isAllowed = (header: DatalistTableHeader) =>
28
+ !header.access || !!unref(header.access());
29
+
30
+ const allowedHeaders = rawHeaders.filter(isAllowed);
31
+
32
+ /* headers may share a `field`, so it stays allowed while any of them is */
33
+ const allowedFields = new Set(allowedHeaders.map((header) => header.field));
34
+ const deniedFields = new Set(
35
+ rawHeaders
36
+ .filter((header) => !isAllowed(header))
37
+ .map((header) => header.field)
38
+ .filter((field) => !allowedFields.has(field)),
39
+ );
40
+
41
+ const headers = ref<DatalistTableHeader[]>(allowedHeaders);
28
42
  const isReorderingColumn = ref(false);
29
43
 
30
44
  const shownHeaders = computed(() => {
@@ -64,7 +78,7 @@ export const tableHeadersStoreBody = ({
64
78
  });
65
79
 
66
80
  const $reset = () => {
67
- headers.value = rawHeaders;
81
+ headers.value = allowedHeaders;
68
82
  };
69
83
 
70
84
  const updateShownHeaders = (value: DatalistTableHeader[]) => {
@@ -112,7 +126,10 @@ export const tableHeadersStoreBody = ({
112
126
  });
113
127
  };
114
128
 
115
- const updateFields = (fields: string[]) => {
129
+ const updateFields = (persistedFields: string[]) => {
130
+ /* persisted state predates the gate, and unknown fields are revived below */
131
+ const fields = persistedFields.filter((field) => !deniedFields.has(field));
132
+
116
133
  const fieldsSet = new Set(fields);
117
134
  const mainFieldNames = new Set(headers.value.map((header) => header.field));
118
135
 
@@ -13,6 +13,13 @@ import type { DatalistStoreProviderType } from './StoreProvider';
13
13
  export type DatalistTableHeader = WtTableHeader & {
14
14
  field: string;
15
15
  shouldBeInitialized?: boolean;
16
+ /**
17
+ * Access gate supplied by the consuming app, evaluated once when the headers
18
+ * store is created. Returning `false` drops the column completely: absent
19
+ * from the column picker, never rendered, never requested, and not
20
+ * restorable from persisted state. Omitted means allowed.
21
+ */
22
+ access?: () => boolean | Ref<boolean>;
16
23
  };
17
24
 
18
25
  export type TrackSelectedRowBy<T> = (row: T) => T;