@webitel/ui-datalist 26.8.4 → 26.8.5
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/index.ts +2 -1
- package/src/modules/card/composables/useCardComponent.ts +9 -0
- package/src/modules/card/composables/useCardStoreProvider.ts +26 -0
- package/src/modules/card/index.ts +1 -0
- package/src/modules/card/stores/__tests__/createCardStore.spec.ts +73 -0
- package/src/modules/card/stores/createCardStore.ts +22 -1
- package/src/modules/card/types/CardStore.types.ts +8 -0
- package/src/modules/table/__tests__/createTableStore.spec.ts +76 -0
- package/src/modules/table/__tests__/useNestedTableList.spec.ts +218 -0
- package/src/modules/table/composables/useNestedTableList.ts +57 -0
- package/src/modules/table/createTableStore.store.ts +37 -0
- package/types/.tsbuildinfo +1 -1
- package/types/index.d.ts +2 -1
- package/types/modules/card/composables/useCardStoreProvider.d.ts +18 -0
- package/types/modules/card/index.d.ts +1 -0
- package/types/modules/card/stores/createCardStore.d.ts +10 -1
- package/types/modules/card/types/CardStore.types.d.ts +7 -0
- package/types/modules/filter-presets/stores/createFilterPresetsStore.d.ts +2 -0
- package/types/modules/permissions-page/stores/createPermissionsStore.d.ts +2 -0
- package/types/modules/table/composables/useNestedTableList.d.ts +27 -0
- package/types/modules/table/createTableStore.store.d.ts +4 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useNestedTableList } from './modules/table/composables/useNestedTableList';
|
|
1
2
|
import { createTableStore } from './modules/table/createTableStore.store';
|
|
2
3
|
import type { DatalistTableHeader } from './modules/types/tableStore.types';
|
|
3
4
|
|
|
4
5
|
export type { DatalistTableHeader };
|
|
5
|
-
export { createTableStore };
|
|
6
|
+
export { createTableStore, useNestedTableList };
|
|
@@ -7,6 +7,10 @@ import { useCardAnyFieldEditedWatcher } from './useCardAnyFieldEditedWatcher';
|
|
|
7
7
|
import { useCardIsNew } from './useCardIsNew';
|
|
8
8
|
import { useCardRouting } from './useCardRouting';
|
|
9
9
|
import { useCardSaveAction } from './useCardSaveAction';
|
|
10
|
+
import {
|
|
11
|
+
type NestedListsOwner,
|
|
12
|
+
provideCardStore,
|
|
13
|
+
} from './useCardStoreProvider';
|
|
10
14
|
import { useCardValidation } from './useCardValidation';
|
|
11
15
|
import { useItemCardSaveText } from './useItemCardSaveText';
|
|
12
16
|
|
|
@@ -31,6 +35,11 @@ export const useCardComponent = <
|
|
|
31
35
|
}) => {
|
|
32
36
|
const cardStore = useCardStore();
|
|
33
37
|
|
|
38
|
+
// the nested tabs register their lists here, and the card empties them.
|
|
39
|
+
// `useCardStore` is a bare StoreDefinition, as everywhere else in here, so
|
|
40
|
+
// the store's own shape has to be asserted rather than inferred
|
|
41
|
+
provideCardStore(cardStore as unknown as NestedListsOwner);
|
|
42
|
+
|
|
34
43
|
const {
|
|
35
44
|
itemId,
|
|
36
45
|
originalItemInstance,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type InjectionKey, inject, provide } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { CardItemId, NestedList } from '../types/CardStore.types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a nested tab needs from the card page it lives in: the record it is a
|
|
7
|
+
* tab of, and somewhere to register its list so the card can empty it.
|
|
8
|
+
*/
|
|
9
|
+
export interface NestedListsOwner {
|
|
10
|
+
itemId: CardItemId;
|
|
11
|
+
registerNestedList: (list: NestedList) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CardStoreKey: InjectionKey<NestedListsOwner> =
|
|
15
|
+
Symbol('datalistCardStore');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `useCardComponent` provides the card's own store, so the tabs below it need
|
|
19
|
+
* no wiring of their own — see `useNestedTableList`.
|
|
20
|
+
*/
|
|
21
|
+
export const provideCardStore = (store: NestedListsOwner) => {
|
|
22
|
+
provide(CardStoreKey, store);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** `null` outside a card page, where a list has no owner to reset it */
|
|
26
|
+
export const useInjectedCardStore = () => inject(CardStoreKey, null);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './composables/useCardAnyFieldEditedWatcher';
|
|
2
2
|
export * from './composables/useCardComponent';
|
|
3
3
|
export * from './composables/useCardListNavigation';
|
|
4
|
+
export * from './composables/useCardStoreProvider';
|
|
4
5
|
export * from './composables/useCardTabs';
|
|
5
6
|
export * from './composables/useNestedCardComponent';
|
|
6
7
|
export * from './stores/createCardStore';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
|
|
5
|
+
import { createCardStore } from '../createCardStore';
|
|
6
|
+
|
|
7
|
+
const cardStoreOf = (namespace: string) =>
|
|
8
|
+
createCardStore({
|
|
9
|
+
namespace,
|
|
10
|
+
apiModule: {
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
add: vi.fn(),
|
|
13
|
+
update: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
standardValidationSchema: z.object({
|
|
16
|
+
name: z.string().optional(),
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
The lists of a card's nested tabs live in stores shared by every card of their
|
|
22
|
+
kind, so the card that filled them is the one that has to empty them.
|
|
23
|
+
|
|
24
|
+
[WTEL-10350](https://webitel.atlassian.net/browse/WTEL-10350)
|
|
25
|
+
*/
|
|
26
|
+
describe('createCardStore nested lists', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
setActivePinia(createPinia());
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('empties every registered list on reset', () => {
|
|
32
|
+
const store = cardStoreOf('cases/card')();
|
|
33
|
+
const lists = [
|
|
34
|
+
{
|
|
35
|
+
$reset: vi.fn(),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
$reset: vi.fn(),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const list of lists) store.registerNestedList(list);
|
|
43
|
+
store.$reset();
|
|
44
|
+
|
|
45
|
+
for (const list of lists) expect(list.$reset).toHaveBeenCalledOnce();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('forgets them, so the next card resets only its own', () => {
|
|
49
|
+
const store = cardStoreOf('contacts/card')();
|
|
50
|
+
const list = {
|
|
51
|
+
$reset: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
store.registerNestedList(list);
|
|
55
|
+
store.$reset();
|
|
56
|
+
store.$reset();
|
|
57
|
+
|
|
58
|
+
expect(list.$reset).toHaveBeenCalledOnce();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('registers a list once, however many times its tab is mounted', () => {
|
|
62
|
+
const store = cardStoreOf('queues/card')();
|
|
63
|
+
const list = {
|
|
64
|
+
$reset: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
store.registerNestedList(list);
|
|
68
|
+
store.registerNestedList(list);
|
|
69
|
+
store.$reset();
|
|
70
|
+
|
|
71
|
+
expect(list.$reset).toHaveBeenCalledOnce();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -10,7 +10,11 @@ import type { MaybeRef } from 'vue';
|
|
|
10
10
|
import { effectScope, ref, toRaw, toValue, watch } from 'vue';
|
|
11
11
|
import type { z } from 'zod/v4';
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
CardItemId,
|
|
15
|
+
CardParentId,
|
|
16
|
+
NestedList,
|
|
17
|
+
} from '../types/CardStore.types';
|
|
14
18
|
|
|
15
19
|
const defaultRegleValidationOptions: RegleSchemaBehaviourOptions &
|
|
16
20
|
RegleBehaviourOptions = {
|
|
@@ -167,6 +171,19 @@ export const createCardStore = <
|
|
|
167
171
|
}
|
|
168
172
|
};
|
|
169
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The lists of the card's nested tabs. They live in stores of their own,
|
|
176
|
+
* shared by every card of this kind and outliving all of them, so the card
|
|
177
|
+
* that filled them is the one that has to empty them.
|
|
178
|
+
*
|
|
179
|
+
* [WTEL-10350](https://webitel.atlassian.net/browse/WTEL-10350)
|
|
180
|
+
*/
|
|
181
|
+
const nestedLists = new Set<NestedList>();
|
|
182
|
+
|
|
183
|
+
const registerNestedList = (list: NestedList) => {
|
|
184
|
+
nestedLists.add(list);
|
|
185
|
+
};
|
|
186
|
+
|
|
170
187
|
const initialize = ({
|
|
171
188
|
itemId: initialItemId,
|
|
172
189
|
parentId: initialParentId,
|
|
@@ -186,6 +203,9 @@ export const createCardStore = <
|
|
|
186
203
|
};
|
|
187
204
|
|
|
188
205
|
const $reset = () => {
|
|
206
|
+
for (const list of nestedLists) list.$reset();
|
|
207
|
+
nestedLists.clear();
|
|
208
|
+
|
|
189
209
|
itemId.value = null;
|
|
190
210
|
parentId.value = null;
|
|
191
211
|
createValidationSchema();
|
|
@@ -212,6 +232,7 @@ export const createCardStore = <
|
|
|
212
232
|
initialize,
|
|
213
233
|
saveItem,
|
|
214
234
|
$reset,
|
|
235
|
+
registerNestedList,
|
|
215
236
|
};
|
|
216
237
|
});
|
|
217
238
|
};
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
export type CardItemId = string | number | null;
|
|
2
2
|
export type CardParentId = CardItemId;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a card store needs from the list of one of its nested tabs: the ability
|
|
6
|
+
* to empty it when the card page goes away.
|
|
7
|
+
*/
|
|
8
|
+
export interface NestedList {
|
|
9
|
+
$reset: () => void;
|
|
10
|
+
}
|
|
@@ -183,4 +183,80 @@ describe('tableStoreBody', () => {
|
|
|
183
183
|
expect(getList).toHaveBeenCalled();
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
|
+
/*
|
|
187
|
+
A nested list is emptied by the card store it is registered with, and until
|
|
188
|
+
it has a parent again it must not load: the store keeps the parent it was
|
|
189
|
+
initialized with, so a reload in between queried the previous card's record.
|
|
190
|
+
|
|
191
|
+
[WTEL-10350](https://webitel.atlassian.net/browse/WTEL-10350)
|
|
192
|
+
*/
|
|
193
|
+
describe('$reset of a nested list', () => {
|
|
194
|
+
let useStore: ReturnType<typeof createTableStore>;
|
|
195
|
+
|
|
196
|
+
beforeEach(async () => {
|
|
197
|
+
useStore = createTableStore('cases-reset/datalist', {
|
|
198
|
+
apiModule: {
|
|
199
|
+
getList,
|
|
200
|
+
},
|
|
201
|
+
headers,
|
|
202
|
+
disablePersistence: true,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
mountTab(useStore);
|
|
206
|
+
await flush();
|
|
207
|
+
getList.mockClear();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('drops the rows, the parent and the pagination', async () => {
|
|
211
|
+
const store = useStore();
|
|
212
|
+
store.updatePage(3);
|
|
213
|
+
await flush();
|
|
214
|
+
|
|
215
|
+
store.$reset();
|
|
216
|
+
|
|
217
|
+
expect(store.dataList).toEqual([]);
|
|
218
|
+
expect(store.selected).toEqual([]);
|
|
219
|
+
expect(store.error).toBeNull();
|
|
220
|
+
expect(store.page).toBe(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('keeps the headers, which are the user column choice', async () => {
|
|
224
|
+
const store = useStore();
|
|
225
|
+
store.updateShownHeaders(
|
|
226
|
+
store.headers.map((header, index) => ({
|
|
227
|
+
...header,
|
|
228
|
+
show: index === 0,
|
|
229
|
+
})),
|
|
230
|
+
);
|
|
231
|
+
await flush();
|
|
232
|
+
const shown = store.headers.map((header) => header.show);
|
|
233
|
+
|
|
234
|
+
store.$reset();
|
|
235
|
+
|
|
236
|
+
expect(store.headers.map((header) => header.show)).toEqual(shown);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('loads nothing until it has a parent again', async () => {
|
|
240
|
+
const store = useStore();
|
|
241
|
+
store.$reset();
|
|
242
|
+
getList.mockClear();
|
|
243
|
+
|
|
244
|
+
await store.loadDataList();
|
|
245
|
+
store.updatePage(2);
|
|
246
|
+
await flush();
|
|
247
|
+
|
|
248
|
+
expect(getList).not.toHaveBeenCalled();
|
|
249
|
+
|
|
250
|
+
store.initialize({
|
|
251
|
+
parentId: '43',
|
|
252
|
+
});
|
|
253
|
+
await flush();
|
|
254
|
+
|
|
255
|
+
expect(getList).toHaveBeenCalledWith(
|
|
256
|
+
expect.objectContaining({
|
|
257
|
+
parentId: '43',
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
186
262
|
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { defineComponent, h, ref } from 'vue';
|
|
5
|
+
import { createMemoryHistory, createRouter, type Router } from 'vue-router';
|
|
6
|
+
|
|
7
|
+
import { provideCardStore } from '../../card/composables/useCardStoreProvider';
|
|
8
|
+
import type { CardItemId } from '../../card/types/CardStore.types';
|
|
9
|
+
import type { DatalistTableHeader } from '../../types/tableStore.types';
|
|
10
|
+
import { useNestedTableList } from '../composables/useNestedTableList';
|
|
11
|
+
import { createTableStore } from '../createTableStore.store';
|
|
12
|
+
|
|
13
|
+
const routes = [
|
|
14
|
+
{
|
|
15
|
+
path: '/queues',
|
|
16
|
+
name: 'queues',
|
|
17
|
+
component: {
|
|
18
|
+
template: '<div />',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const headers = [
|
|
24
|
+
{
|
|
25
|
+
value: 'name',
|
|
26
|
+
field: 'name',
|
|
27
|
+
show: true,
|
|
28
|
+
sort: null,
|
|
29
|
+
},
|
|
30
|
+
] as DatalistTableHeader[];
|
|
31
|
+
|
|
32
|
+
const flush = async () => {
|
|
33
|
+
for (let i = 0; i < 5; i += 1) {
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
A nested list store is shared by every card of its kind and outlives all of
|
|
40
|
+
them, so a tab that read it directly showed the rows of whichever record was
|
|
41
|
+
opened before — and a card for an unsaved record showed them until saved.
|
|
42
|
+
|
|
43
|
+
[WTEL-10350](https://webitel.atlassian.net/browse/WTEL-10350)
|
|
44
|
+
*/
|
|
45
|
+
describe('useNestedTableList', () => {
|
|
46
|
+
let router: Router;
|
|
47
|
+
let getList: ReturnType<typeof vi.fn>;
|
|
48
|
+
let useStore: ReturnType<typeof createTableStore>;
|
|
49
|
+
let namespace = 0;
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
localStorage.clear();
|
|
53
|
+
sessionStorage.clear();
|
|
54
|
+
setActivePinia(createPinia());
|
|
55
|
+
|
|
56
|
+
router = createRouter({
|
|
57
|
+
history: createMemoryHistory(),
|
|
58
|
+
routes,
|
|
59
|
+
});
|
|
60
|
+
await router.push('/queues');
|
|
61
|
+
await router.isReady();
|
|
62
|
+
|
|
63
|
+
getList = vi.fn().mockImplementation(({ parentId }) =>
|
|
64
|
+
Promise.resolve({
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
id: 1,
|
|
68
|
+
name: `row of ${parentId}`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
next: false,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
namespace += 1;
|
|
76
|
+
useStore = createTableStore(`queue-buckets-${namespace}/datalist`, {
|
|
77
|
+
apiModule: {
|
|
78
|
+
getList,
|
|
79
|
+
},
|
|
80
|
+
headers,
|
|
81
|
+
disablePersistence: true,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** a card page with one nested tab in it */
|
|
86
|
+
const openCard = (itemId: CardItemId) => {
|
|
87
|
+
const cardItemId = ref<CardItemId>(itemId);
|
|
88
|
+
const nestedLists = new Set<{
|
|
89
|
+
$reset: () => void;
|
|
90
|
+
}>();
|
|
91
|
+
|
|
92
|
+
const Tab = defineComponent({
|
|
93
|
+
setup() {
|
|
94
|
+
useNestedTableList({
|
|
95
|
+
useTableStore: useStore,
|
|
96
|
+
});
|
|
97
|
+
return () => h('div');
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const Card = defineComponent({
|
|
102
|
+
setup() {
|
|
103
|
+
provideCardStore({
|
|
104
|
+
get itemId() {
|
|
105
|
+
return cardItemId.value;
|
|
106
|
+
},
|
|
107
|
+
registerNestedList: (list) => nestedLists.add(list),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return () => h(Tab);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const card = mount(Card, {
|
|
115
|
+
global: {
|
|
116
|
+
plugins: [
|
|
117
|
+
router,
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** what the card page does on unmount */
|
|
123
|
+
const close = () => {
|
|
124
|
+
for (const list of nestedLists) list.$reset();
|
|
125
|
+
nestedLists.clear();
|
|
126
|
+
card.unmount();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
card,
|
|
131
|
+
close,
|
|
132
|
+
cardItemId,
|
|
133
|
+
nestedLists,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
it('loads the list of the card it is opened in', async () => {
|
|
138
|
+
openCard('42');
|
|
139
|
+
await flush();
|
|
140
|
+
|
|
141
|
+
expect(getList).toHaveBeenCalledWith(
|
|
142
|
+
expect.objectContaining({
|
|
143
|
+
parentId: '42',
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
expect(useStore().dataList).toEqual([
|
|
147
|
+
{
|
|
148
|
+
id: 1,
|
|
149
|
+
name: 'row of 42',
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('registers itself with the card, which empties it on close', async () => {
|
|
155
|
+
const first = openCard('42');
|
|
156
|
+
await flush();
|
|
157
|
+
expect(first.nestedLists.size).toBe(1);
|
|
158
|
+
|
|
159
|
+
first.close();
|
|
160
|
+
await flush();
|
|
161
|
+
|
|
162
|
+
expect(useStore().dataList).toEqual([]);
|
|
163
|
+
expect(first.nestedLists.size).toBe(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('shows no rows of the previously opened card', async () => {
|
|
167
|
+
const first = openCard('42');
|
|
168
|
+
await flush();
|
|
169
|
+
first.close();
|
|
170
|
+
await flush();
|
|
171
|
+
|
|
172
|
+
getList.mockClear();
|
|
173
|
+
const second = openCard(null); // a record that is not saved yet
|
|
174
|
+
await flush();
|
|
175
|
+
|
|
176
|
+
expect(getList).not.toHaveBeenCalled();
|
|
177
|
+
expect(useStore().dataList).toEqual([]);
|
|
178
|
+
second.close();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('loads once the unsaved record gets its id', async () => {
|
|
182
|
+
const card = openCard(null);
|
|
183
|
+
await flush();
|
|
184
|
+
expect(getList).not.toHaveBeenCalled();
|
|
185
|
+
|
|
186
|
+
card.cardItemId.value = '43';
|
|
187
|
+
await flush();
|
|
188
|
+
|
|
189
|
+
expect(getList).toHaveBeenCalledWith(
|
|
190
|
+
expect.objectContaining({
|
|
191
|
+
parentId: '43',
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
expect(useStore().dataList).toEqual([
|
|
195
|
+
{
|
|
196
|
+
id: 1,
|
|
197
|
+
name: 'row of 43',
|
|
198
|
+
},
|
|
199
|
+
]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('does not reload the previous parent when the store reloads itself', async () => {
|
|
203
|
+
const first = openCard('42');
|
|
204
|
+
await flush();
|
|
205
|
+
first.close();
|
|
206
|
+
await flush();
|
|
207
|
+
|
|
208
|
+
getList.mockClear();
|
|
209
|
+
const store = useStore();
|
|
210
|
+
|
|
211
|
+
// what a pagination change does, with no card to belong to
|
|
212
|
+
store.updatePage(2);
|
|
213
|
+
await flush();
|
|
214
|
+
|
|
215
|
+
expect(getList).not.toHaveBeenCalled();
|
|
216
|
+
expect(store.dataList).toEqual([]);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { StoreDefinition } from 'pinia';
|
|
2
|
+
import { computed, type MaybeRefOrGetter, toValue, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { useInjectedCardStore } from '../../card/composables/useCardStoreProvider';
|
|
5
|
+
import type { CardItemId } from '../../card/types/CardStore.types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The list of a card page's nested tab.
|
|
9
|
+
*
|
|
10
|
+
* The store is shared by every card of its kind and outlives all of them, so a
|
|
11
|
+
* tab that read it directly showed the rows of whichever record was opened
|
|
12
|
+
* before — and a card for an unsaved record, having no id to initialize with,
|
|
13
|
+
* showed them until it was saved.
|
|
14
|
+
*
|
|
15
|
+
* Registering the list with the card store makes the card responsible for
|
|
16
|
+
* emptying it, and the parent to load comes from the card's own `itemId`: it
|
|
17
|
+
* arrives late for a record created from a nested tab, and the list follows.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const tableStore = useNestedTableList({ useTableStore: useQueueBucketsStore });
|
|
21
|
+
* const { dataList, isLoading } = storeToRefs(tableStore);
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* [WTEL-10350](https://webitel.atlassian.net/browse/WTEL-10350)
|
|
25
|
+
*/
|
|
26
|
+
export const useNestedTableList = ({
|
|
27
|
+
useTableStore,
|
|
28
|
+
parentId,
|
|
29
|
+
}: {
|
|
30
|
+
useTableStore: StoreDefinition;
|
|
31
|
+
/** only where the parent is not the card's own record */
|
|
32
|
+
parentId?: MaybeRefOrGetter<CardItemId>;
|
|
33
|
+
}) => {
|
|
34
|
+
const tableStore = useTableStore();
|
|
35
|
+
const cardStore = useInjectedCardStore();
|
|
36
|
+
|
|
37
|
+
cardStore?.registerNestedList(tableStore);
|
|
38
|
+
|
|
39
|
+
const parent = computed<CardItemId>(
|
|
40
|
+
() => toValue(parentId) ?? cardStore?.itemId ?? null,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
watch(
|
|
44
|
+
parent,
|
|
45
|
+
(id) => {
|
|
46
|
+
if (id)
|
|
47
|
+
tableStore.initialize({
|
|
48
|
+
parentId: id,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
immediate: true,
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return tableStore;
|
|
57
|
+
};
|
|
@@ -34,6 +34,13 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
34
34
|
const useFiltersStore = createTableFiltersStore(namespace, config);
|
|
35
35
|
|
|
36
36
|
const parentId = ref();
|
|
37
|
+
/**
|
|
38
|
+
* A list initialized with a `parentId` is nested in a card page, and stays
|
|
39
|
+
* nested for the store's lifetime — the api module addresses its parent.
|
|
40
|
+
* `parentId` is dropped on `$reset`, this is not: it is what tells a reset
|
|
41
|
+
* store apart from a registry one that never had a parent.
|
|
42
|
+
*/
|
|
43
|
+
const isNested = ref(false);
|
|
37
44
|
|
|
38
45
|
const paginationStore = usePaginationStore();
|
|
39
46
|
const { page, size, next } = makeThisToRefs<typeof paginationStore>(
|
|
@@ -129,6 +136,15 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
129
136
|
const loadDataList = async ({
|
|
130
137
|
withLoading = true,
|
|
131
138
|
}: LoadDataListOptions = {}) => {
|
|
139
|
+
/*
|
|
140
|
+
a nested list with no parent has nothing to address: either its card was
|
|
141
|
+
left (the card store reset it) or the record is not saved yet. Loading
|
|
142
|
+
here would query whatever parent the store held before.
|
|
143
|
+
|
|
144
|
+
[WTEL-10350](https://webitel.atlassian.net/browse/WTEL-10350)
|
|
145
|
+
*/
|
|
146
|
+
if (isNested.value && !parentId.value) return;
|
|
147
|
+
|
|
132
148
|
if (withLoading) {
|
|
133
149
|
isLoading.value = true;
|
|
134
150
|
}
|
|
@@ -344,6 +360,7 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
344
360
|
} = {}) => {
|
|
345
361
|
if (storeParentId) {
|
|
346
362
|
parentId.value = storeParentId;
|
|
363
|
+
isNested.value = true;
|
|
347
364
|
}
|
|
348
365
|
|
|
349
366
|
const isStoreAlreadySetUp = isStoreSetUp.value;
|
|
@@ -365,6 +382,24 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
365
382
|
return loadDataList();
|
|
366
383
|
};
|
|
367
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Drops everything that belonged to one parent record. Called by the card
|
|
387
|
+
* store this list is registered with, when the card page goes away.
|
|
388
|
+
*
|
|
389
|
+
* Headers are left alone — they are the user's column choice, restored once
|
|
390
|
+
* per app lifetime — and so is `isStoreSetUp`, for the same reason.
|
|
391
|
+
*/
|
|
392
|
+
const $reset = () => {
|
|
393
|
+
dataList.value = [];
|
|
394
|
+
selected.value = [];
|
|
395
|
+
error.value = null;
|
|
396
|
+
isLoading.value = false;
|
|
397
|
+
parentId.value = undefined;
|
|
398
|
+
|
|
399
|
+
paginationStore.$reset();
|
|
400
|
+
filtersManager.value.reset();
|
|
401
|
+
};
|
|
402
|
+
|
|
368
403
|
const resetInfiniteScrollTableParamsToDefaults = () => {
|
|
369
404
|
paginationStore.$reset();
|
|
370
405
|
filtersManager.value.reset();
|
|
@@ -373,6 +408,7 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
373
408
|
|
|
374
409
|
return {
|
|
375
410
|
isStoreSetUp, // internal export for pinia devtools
|
|
411
|
+
isNested, // internal export for pinia devtools
|
|
376
412
|
|
|
377
413
|
dataList,
|
|
378
414
|
selected,
|
|
@@ -395,6 +431,7 @@ export const tableStoreBody = <Entity extends Identifiable>(
|
|
|
395
431
|
|
|
396
432
|
setupStore, // only setup, no data loading
|
|
397
433
|
initialize, // setup + load data
|
|
434
|
+
$reset, // drop the data and the parent binding
|
|
398
435
|
syncPersistence, // republish store state into the route query
|
|
399
436
|
|
|
400
437
|
loadDataList,
|