@webitel/ui-datalist 1.1.65 → 1.1.67

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.
Files changed (24) hide show
  1. package/package.json +1 -1
  2. package/src/modules/filters/__tests__/createTableFiltersStore.spec.ts +143 -0
  3. package/src/modules/filters/createTableFiltersStore.ts +23 -5
  4. package/src/modules/filters/modules/filterConfig/components/contact-group/contact-group-filter-value-field.vue +40 -25
  5. package/src/modules/headers/__tests__/createTableHeadersStore.spec.ts +169 -0
  6. package/src/modules/headers/createTableHeadersStore.ts +26 -7
  7. package/src/modules/pagination/__tests__/createTablePaginationStore.spec.ts +134 -0
  8. package/src/modules/pagination/createTablePaginationStore.ts +32 -4
  9. package/src/modules/persist/PersistedStorage.types.ts +14 -0
  10. package/src/modules/persist/__tests__/useLocalStoragePersistedStorage.spec.ts +66 -0
  11. package/src/modules/persist/__tests__/usePersistedStorage.spec.ts +357 -0
  12. package/src/modules/persist/__tests__/useRoutePersistedStorage.spec.ts +145 -0
  13. package/src/modules/persist/usePersistedStorage.ts +93 -60
  14. package/src/modules/persist/useRoutePersistedStorage.ts +40 -18
  15. package/src/modules/table/createTableStore.store.ts +37 -0
  16. package/types/.tsbuildinfo +1 -1
  17. package/types/modules/filter-presets/stores/createFilterPresetsStore.d.ts +1 -0
  18. package/types/modules/filters/createTableFiltersStore.d.ts +2 -0
  19. package/types/modules/filters/modules/filterConfig/components/contact-group/contact-group-filter-value-field.vue.d.ts +4 -4
  20. package/types/modules/headers/createTableHeadersStore.d.ts +2 -0
  21. package/types/modules/pagination/createTablePaginationStore.d.ts +2 -0
  22. package/types/modules/permissions-page/stores/createPermissionsStore.d.ts +2 -0
  23. package/types/modules/persist/PersistedStorage.types.d.ts +13 -0
  24. package/types/modules/table/createTableStore.store.d.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/ui-datalist",
3
- "version": "1.1.65",
3
+ "version": "1.1.67",
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",
@@ -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 { PersistedStorageType } from '../persist/PersistedStorage.types';
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 { restore: restoreFilters } = usePersistedStorage({
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 { restore: restoreSearchMode } = usePersistedStorage({
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
- restoreFilters(),
100
- restoreSearchMode(),
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
 
@@ -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,
@@ -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 { PersistedStorageType } from '../persist/PersistedStorage.types';
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 { restore: restoreFields } = usePersistedStorage({
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 { restore: restoreSort } = usePersistedStorage({
255
+ const sortStorage = usePersistedStorage({
251
256
  name: 'sort',
252
257
  value: sort,
253
258
  });
254
259
 
255
- const { restore: restoreColumnWidths } = usePersistedStorage({
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
- restoreFields(),
283
- restoreSort(),
284
- restoreColumnWidths(),
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
  };