@webitel/ui-datalist 1.1.60 → 1.1.62
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/filter-presets/components/apply-preset/__tests__/apply-preset-action.spec.ts +119 -0
- package/src/modules/filter-presets/components/apply-preset/apply-preset-action.vue +23 -1
- package/src/modules/filters/components/__tests__/table-filters-panel.spec.ts +100 -0
- package/src/modules/filters/components/config/static-view/static-filter-field.vue +2 -1
- package/src/modules/filters/components/table-filters-panel.vue +28 -3
- package/src/modules/filters/components/types/Filter.types.ts +2 -6
- package/types/.tsbuildinfo +1 -1
- package/types/modules/filter-presets/components/apply-preset/apply-preset-action.vue.d.ts +8 -0
- package/types/modules/filters/components/config/static-view/static-filter-field.vue.d.ts +2 -2
- package/types/modules/filters/components/preview/dynamic-filter-preview.vue.d.ts +3 -2
- package/types/modules/filters/components/table-filters-panel.vue.d.ts +7 -5
- package/types/modules/filters/components/types/Filter.types.d.ts +2 -2
package/package.json
CHANGED
package/src/modules/filter-presets/components/apply-preset/__tests__/apply-preset-action.spec.ts
ADDED
|
@@ -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
|
-
|
|
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
|
|
@@ -74,7 +76,7 @@ import { computed } from 'vue';
|
|
|
74
76
|
import { WebitelProtoDataField } from 'webitel-sdk';
|
|
75
77
|
|
|
76
78
|
import { ApplyPresetAction, SavePresetAction } from '../../filter-presets';
|
|
77
|
-
import {
|
|
79
|
+
import { FilterInitParams, IFilter } from '../classes/Filter';
|
|
78
80
|
import { IFiltersManager } from '../classes/FiltersManager';
|
|
79
81
|
import { useFilterConfigsToolkit } from '../composables/useFilterConfigsToolkit';
|
|
80
82
|
import { AnyFilterConfig } from '../modules/filterConfig/classes/FilterConfig';
|
|
@@ -149,10 +151,10 @@ const props = defineProps<Props>();
|
|
|
149
151
|
*/
|
|
150
152
|
const emit = defineEmits<{
|
|
151
153
|
'filter:add': [
|
|
152
|
-
|
|
154
|
+
FilterInitParams,
|
|
153
155
|
];
|
|
154
156
|
'filter:update': [
|
|
155
|
-
|
|
157
|
+
FilterInitParams,
|
|
156
158
|
];
|
|
157
159
|
'filter:delete': [
|
|
158
160
|
IFilter,
|
|
@@ -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 = () => {
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
FilterData,
|
|
3
|
-
FilterInitParams,
|
|
4
|
-
IFilter,
|
|
5
|
-
} from '../../classes/Filter';
|
|
1
|
+
import type { FilterInitParams, IFilter } from '../../classes/Filter';
|
|
6
2
|
import type { AnyFilterConfig } from '../../modules/filterConfig';
|
|
7
3
|
|
|
8
4
|
export interface FilterEmits {
|
|
9
5
|
'update:filter': [
|
|
10
|
-
|
|
6
|
+
FilterInitParams,
|
|
11
7
|
];
|
|
12
8
|
'delete:filter': [
|
|
13
9
|
IFilter,
|