@webitel/ui-datalist 1.1.60 → 1.1.61

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.60",
3
+ "version": "1.1.61",
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,119 @@
1
+ import { createTestingPinia } from '@pinia/testing';
2
+ import { flushPromises, shallowMount } from '@vue/test-utils';
3
+ import { defineStore } from 'pinia';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { ref } from 'vue';
6
+
7
+ import { createFiltersManager } from '../../../../filters/classes/FiltersManager';
8
+ import PresetQueryAPI from '../../../api/PresetQuery';
9
+ import ApplyPresetAction from '../apply-preset-action.vue';
10
+
11
+ vi.mock('../../../api/PresetQuery', () => ({
12
+ default: {
13
+ get: vi.fn(),
14
+ getList: vi.fn(),
15
+ update: vi.fn(),
16
+ delete: vi.fn(),
17
+ },
18
+ }));
19
+
20
+ const SNAPSHOT = '{"createdAt_val":"today"}';
21
+ const CACHED_PRESET_ID = 7;
22
+ const STORE_ID = 'test/presets';
23
+
24
+ /*
25
+ Only what apply-preset-action reads from the presets store – createTestingPinia
26
+ stubs the actions, so none of them need bodies.
27
+ */
28
+ const useTestPresetsStore = defineStore(STORE_ID, () => ({
29
+ dataList: ref([]),
30
+ error: ref(null),
31
+ isLoading: ref(false),
32
+ filtersManager: ref(createFiltersManager()),
33
+ presetId: ref<number | null>(null),
34
+
35
+ loadDataList: () => {},
36
+ initialize: () => {},
37
+ updateSize: () => {},
38
+ deleteEls: () => {},
39
+ setupPresetPersistence: () => {},
40
+ }));
41
+
42
+ const mountAction = ({
43
+ hasAnyFilters = false,
44
+ presetId = CACHED_PRESET_ID,
45
+ } = {}) => {
46
+ const pinia = createTestingPinia({
47
+ initialState: {
48
+ [STORE_ID]: {
49
+ presetId,
50
+ },
51
+ },
52
+ });
53
+
54
+ return shallowMount(ApplyPresetAction, {
55
+ props: {
56
+ namespace: 'test',
57
+ presetsStore: useTestPresetsStore(pinia),
58
+ filterConfigs: [],
59
+ hasAnyFilters,
60
+ },
61
+ global: {
62
+ plugins: [
63
+ pinia,
64
+ ],
65
+ },
66
+ });
67
+ };
68
+
69
+ describe('ApplyPresetAction', () => {
70
+ beforeEach(() => {
71
+ vi.clearAllMocks();
72
+
73
+ vi.mocked(PresetQueryAPI.get).mockResolvedValue({
74
+ id: CACHED_PRESET_ID,
75
+ preset: {
76
+ 'filtersManager.toString': SNAPSHOT,
77
+ },
78
+ });
79
+ });
80
+
81
+ it('emits "restore" with the cached preset snapshot when no filters are applied', async () => {
82
+ const wrapper = mountAction();
83
+ await flushPromises();
84
+
85
+ expect(wrapper.emitted('restore')).toEqual([
86
+ [
87
+ SNAPSHOT,
88
+ ],
89
+ ]);
90
+ });
91
+
92
+ it('does not emit "apply" for a cached preset, so filters are not reset', async () => {
93
+ const wrapper = mountAction();
94
+ await flushPromises();
95
+
96
+ expect(wrapper.emitted('apply')).toBeUndefined();
97
+ });
98
+
99
+ it('skips restoring the cached preset when filters are already applied', async () => {
100
+ const wrapper = mountAction({
101
+ hasAnyFilters: true,
102
+ });
103
+ await flushPromises();
104
+
105
+ expect(PresetQueryAPI.get).not.toHaveBeenCalled();
106
+ expect(wrapper.emitted('restore')).toBeUndefined();
107
+ expect(wrapper.emitted('apply')).toBeUndefined();
108
+ });
109
+
110
+ it('does nothing when no preset is cached', async () => {
111
+ const wrapper = mountAction({
112
+ presetId: null,
113
+ });
114
+ await flushPromises();
115
+
116
+ expect(PresetQueryAPI.get).not.toHaveBeenCalled();
117
+ expect(wrapper.emitted('restore')).toBeUndefined();
118
+ });
119
+ });
@@ -99,12 +99,27 @@ const props = defineProps<{
99
99
  namespace: string;
100
100
  presetsStore: StoreGeneric;
101
101
  filterConfigs: AnyFilterConfig[];
102
+ /**
103
+ * @description
104
+ * Filters the user has applied themselves. A cached preset is not restored
105
+ * over them – the filters they arrived with win.
106
+ */
107
+ hasAnyFilters?: boolean;
102
108
  }>();
103
109
 
104
110
  const emit = defineEmits<{
111
+ /**
112
+ * user picked a preset in the popup – consumer resets filters first
113
+ */
105
114
  apply: [
106
115
  string,
107
116
  ];
117
+ /**
118
+ * cached preset re-applied on mount – consumer MUST NOT reset filters
119
+ */
120
+ restore: [
121
+ string,
122
+ ];
108
123
  }>();
109
124
 
110
125
  const eventBus = useEventBus();
@@ -253,12 +268,19 @@ const deletePreset = async (preset: EnginePresetQuery) => {
253
268
 
254
269
  const restorePresetById = async (id: number | null) => {
255
270
  if (!id) return;
271
+ /* cheap exit – skips the request entirely */
272
+ if (props.hasAnyFilters) return;
273
+
256
274
  const presetData = await PresetQueryAPI.get({
257
275
  id,
258
276
  });
259
277
  const filters = presetData?.preset?.['filtersManager.toString'];
260
278
 
261
- if (filters) emit('apply', filters);
279
+ /* filters may have been restored from the route while the preset was being
280
+ fetched, so re-check before overriding them */
281
+ if (props.hasAnyFilters) return;
282
+
283
+ if (filters) emit('restore', filters);
262
284
  };
263
285
 
264
286
  onMounted(async () => {
@@ -0,0 +1,100 @@
1
+ import { createTestingPinia } from '@pinia/testing';
2
+ import { shallowMount } from '@vue/test-utils';
3
+ import { defineStore } from 'pinia';
4
+ import { beforeEach, describe, expect, it } from 'vitest';
5
+
6
+ import ApplyPresetAction from '../../../filter-presets/components/apply-preset/apply-preset-action.vue';
7
+ import {
8
+ createFiltersManager,
9
+ type IFiltersManager,
10
+ } from '../../classes/FiltersManager';
11
+ import { createFilterConfig } from '../../modules/filterConfig/classes/createFilterConfig';
12
+ import { FilterOption } from '../../modules/filterConfig/enums/FilterOption';
13
+ import TableFiltersPanel from '../table-filters-panel.vue';
14
+
15
+ /* the panel only ever calls resetPreset – createTestingPinia stubs it */
16
+ const useTestPresetsStore = defineStore('test/presets', () => ({
17
+ resetPreset: () => {},
18
+ }));
19
+
20
+ /*
21
+ shallowMount stubs dynamic-filter-panel-wrapper, and stubs don't render slots –
22
+ so apply-preset-action would never appear. This stub renders them.
23
+ */
24
+ const DynamicFilterPanelWrapperStub = {
25
+ template: '<div><slot name="filters" /><slot name="actions" /></div>',
26
+ };
27
+
28
+ const filterOptions = [
29
+ /* same shape as cc-history: a seeded, non-removable default */
30
+ createFilterConfig({
31
+ name: FilterOption.CreatedAt,
32
+ notDeletable: true,
33
+ }),
34
+ FilterOption.Agent,
35
+ ];
36
+
37
+ const mountPanel = (filtersManager: IFiltersManager) => {
38
+ const pinia = createTestingPinia();
39
+
40
+ return shallowMount(TableFiltersPanel, {
41
+ props: {
42
+ filterOptions,
43
+ filtersManager,
44
+ presetNamespace: 'test',
45
+ usePresetsStore: useTestPresetsStore,
46
+ },
47
+ global: {
48
+ plugins: [
49
+ pinia,
50
+ ],
51
+ stubs: {
52
+ DynamicFilterPanelWrapper: DynamicFilterPanelWrapperStub,
53
+ },
54
+ },
55
+ });
56
+ };
57
+
58
+ const hasAnyFiltersOf = (filtersManager: IFiltersManager) =>
59
+ mountPanel(filtersManager)
60
+ .findComponent(ApplyPresetAction)
61
+ .props('hasAnyFilters');
62
+
63
+ describe('TableFiltersPanel preset restore gate', () => {
64
+ let filtersManager: IFiltersManager;
65
+
66
+ beforeEach(() => {
67
+ filtersManager = createFiltersManager();
68
+ });
69
+
70
+ it('reports no filters when nothing is applied', () => {
71
+ expect(hasAnyFiltersOf(filtersManager)).toBe(false);
72
+ });
73
+
74
+ it('ignores notDeletable filters, so a seeded default does not block restore', () => {
75
+ filtersManager.addFilter({
76
+ name: FilterOption.CreatedAt,
77
+ value: 'today',
78
+ });
79
+
80
+ expect(hasAnyFiltersOf(filtersManager)).toBe(false);
81
+ });
82
+
83
+ it('ignores the search filter', () => {
84
+ filtersManager.addFilter({
85
+ name: 'search',
86
+ value: 'test',
87
+ });
88
+
89
+ expect(hasAnyFiltersOf(filtersManager)).toBe(false);
90
+ });
91
+
92
+ it('reports filters when a deletable one is applied', () => {
93
+ filtersManager.addFilter({
94
+ name: FilterOption.Agent,
95
+ value: 1,
96
+ });
97
+
98
+ expect(hasAnyFiltersOf(filtersManager)).toBe(true);
99
+ });
100
+ });
@@ -38,9 +38,11 @@
38
38
  <apply-preset-action
39
39
  v-if="presetStore"
40
40
  :filter-configs="filterConfigs"
41
+ :has-any-filters="hasAnyFilters"
41
42
  :namespace="props.presetNamespace ?? ''"
42
43
  :presets-store="presetStore"
43
44
  @apply="emit('preset:apply', $event)"
45
+ @restore="emit('preset:restore', $event)"
44
46
  />
45
47
 
46
48
  <save-preset-action
@@ -164,6 +166,13 @@ const emit = defineEmits<{
164
166
  'preset:apply': [
165
167
  string,
166
168
  ];
169
+ /**
170
+ * string == filtersManager.toString() snapshot of the preset cached in
171
+ * localStorage. Handler must apply it WITHOUT resetting filters
172
+ */
173
+ 'preset:restore': [
174
+ string,
175
+ ];
167
176
  hide: [];
168
177
  }>();
169
178
 
@@ -202,6 +211,22 @@ const listSelectedFilters = computed(() => {
202
211
  );
203
212
  });
204
213
 
214
+ /**
215
+ * @description
216
+ * Filters applied by the user. `notDeletable` configs are skipped – they are
217
+ * seeded defaults, not a user choice, so they must not block preset restore.
218
+ */
219
+ const hasAnyFilters = computed(() =>
220
+ [
221
+ ...listSelectedFilters.value.keys(),
222
+ ].some((name) => {
223
+ const filterConfig = filterConfigs.value.find(
224
+ (config) => String(config.name) === String(name),
225
+ );
226
+ return !filterConfig?.notDeletable;
227
+ }),
228
+ );
229
+
205
230
  const presetStore = props.usePresetsStore ? props.usePresetsStore() : null;
206
231
 
207
232
  const handleResetFilters = () => {