@webitel/ui-datalist 26.8.3 → 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/__tests__/useCardRouting.spec.ts +146 -0
- package/src/modules/card/composables/useCardComponent.ts +9 -0
- package/src/modules/card/composables/useCardRouting.ts +12 -6
- package/src/modules/card/composables/useCardStoreProvider.ts +26 -0
- package/src/modules/card/composables/useNestedCardComponent.ts +8 -5
- 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 +23 -2
- package/src/modules/card/types/CardStore.types.ts +8 -0
- package/src/modules/table/__tests__/createTableStore.spec.ts +262 -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/useCardRouting.d.ts +10 -6
- package/types/modules/card/composables/useCardStoreProvider.d.ts +18 -0
- package/types/modules/card/composables/useNestedCardComponent.d.ts +6 -2
- package/types/modules/card/index.d.ts +1 -0
- package/types/modules/card/stores/createCardStore.d.ts +19 -10
- 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/filters/modules/filterConfig/components/case-service/config.d.ts +1 -1
- 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 };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { effectScope, nextTick, type Ref, reactive, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
import type { CardItemId } from '../../types/CardStore.types';
|
|
5
|
+
import { useCardRouting } from '../useCardRouting';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
route: {
|
|
9
|
+
params: {},
|
|
10
|
+
} as {
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
},
|
|
13
|
+
replace: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('vue-router', () => ({
|
|
17
|
+
useRoute: () => mocks.route,
|
|
18
|
+
useRouter: () => ({
|
|
19
|
+
replace: mocks.replace,
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A nested card lives inside its parent's card page, so the parent can be
|
|
25
|
+
* created underneath it: `route.params.id` goes from `'new'` to a real id while
|
|
26
|
+
* the popup is already mounted. A `parentId` captured as a plain string at
|
|
27
|
+
* setup keeps addressing `'new'`, and every nested request goes to
|
|
28
|
+
* `<entity>/new/<nested>`.
|
|
29
|
+
*
|
|
30
|
+
* [WTEL-10348](https://webitel.atlassian.net/browse/WTEL-10348)
|
|
31
|
+
*/
|
|
32
|
+
describe('useCardRouting', () => {
|
|
33
|
+
const run = (options: {
|
|
34
|
+
itemId: Ref<CardItemId>;
|
|
35
|
+
routeParamName?: string;
|
|
36
|
+
parentId?: (() => CardItemId) | string;
|
|
37
|
+
}) => {
|
|
38
|
+
const scope = effectScope(true);
|
|
39
|
+
const routing = scope.run(() => useCardRouting(options));
|
|
40
|
+
|
|
41
|
+
if (!routing) throw new Error('useCardRouting returned nothing');
|
|
42
|
+
|
|
43
|
+
return routing;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
mocks.replace.mockReset();
|
|
48
|
+
mocks.replace.mockResolvedValue(undefined);
|
|
49
|
+
mocks.route = reactive({
|
|
50
|
+
params: {},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('resolves a getter parent on read instead of capturing it', () => {
|
|
55
|
+
mocks.route.params = {
|
|
56
|
+
id: 'new',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const { parentId } = run({
|
|
60
|
+
itemId: ref(null),
|
|
61
|
+
routeParamName: 'bucketId',
|
|
62
|
+
parentId: () => mocks.route.params.id,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(parentId.value).toBe('new');
|
|
66
|
+
|
|
67
|
+
mocks.route.params = {
|
|
68
|
+
id: '42',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
expect(parentId.value).toBe('42');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('keeps accepting a plain string parent', () => {
|
|
75
|
+
const { parentId } = run({
|
|
76
|
+
itemId: ref(null),
|
|
77
|
+
routeParamName: 'bucketId',
|
|
78
|
+
parentId: '42',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(parentId.value).toBe('42');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('writes the server id into the route of a newly created nested item', async () => {
|
|
85
|
+
mocks.route.params = {
|
|
86
|
+
id: '42',
|
|
87
|
+
bucketId: 'new',
|
|
88
|
+
};
|
|
89
|
+
const itemId = ref<CardItemId>(null);
|
|
90
|
+
|
|
91
|
+
run({
|
|
92
|
+
itemId,
|
|
93
|
+
routeParamName: 'bucketId',
|
|
94
|
+
parentId: () => mocks.route.params.id,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
itemId.value = 7;
|
|
98
|
+
await nextTick();
|
|
99
|
+
|
|
100
|
+
expect(mocks.replace).toHaveBeenCalledWith({
|
|
101
|
+
params: {
|
|
102
|
+
id: '42',
|
|
103
|
+
bucketId: 7,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('leaves the route alone for a nested item that already had an id', async () => {
|
|
109
|
+
mocks.route.params = {
|
|
110
|
+
id: '42',
|
|
111
|
+
bucketId: '7',
|
|
112
|
+
};
|
|
113
|
+
const itemId = ref<CardItemId>(null);
|
|
114
|
+
|
|
115
|
+
run({
|
|
116
|
+
itemId,
|
|
117
|
+
routeParamName: 'bucketId',
|
|
118
|
+
parentId: () => mocks.route.params.id,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
itemId.value = 7;
|
|
122
|
+
await nextTick();
|
|
123
|
+
|
|
124
|
+
expect(mocks.replace).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('writes the server id into the route of a top-level card', async () => {
|
|
128
|
+
mocks.route.params = {
|
|
129
|
+
id: 'new',
|
|
130
|
+
};
|
|
131
|
+
const itemId = ref<CardItemId>(null);
|
|
132
|
+
|
|
133
|
+
run({
|
|
134
|
+
itemId,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
itemId.value = 42;
|
|
138
|
+
await nextTick();
|
|
139
|
+
|
|
140
|
+
expect(mocks.replace).toHaveBeenCalledWith({
|
|
141
|
+
params: {
|
|
142
|
+
id: 42,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -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,
|
|
@@ -1,35 +1,41 @@
|
|
|
1
|
-
import { computed, type Ref, watch } from 'vue';
|
|
1
|
+
import { computed, type MaybeRefOrGetter, type Ref, toValue, watch } from 'vue';
|
|
2
2
|
import { useRoute, useRouter } from 'vue-router';
|
|
3
3
|
|
|
4
|
-
import type { CardItemId } from '../types/CardStore.types';
|
|
4
|
+
import type { CardItemId, CardParentId } from '../types/CardStore.types';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Routing logic shared by top-level and nested card components.
|
|
8
8
|
*
|
|
9
9
|
* - `routeParamName` — which `route.params` key holds the item ID (default: `'id'`)
|
|
10
10
|
* - `parentId` — when provided, the card is treated as nested: URL is updated
|
|
11
|
-
* only when a newly created item receives its server-assigned ID
|
|
11
|
+
* only when a newly created item receives its server-assigned ID. Pass a ref
|
|
12
|
+
* or a getter whenever the parent can be created while the card is already
|
|
13
|
+
* mounted — a popup living inside its parent's card page watches
|
|
14
|
+
* `route.params.id` go from `'new'` to a real id underneath it, and a plain
|
|
15
|
+
* string captured at setup would keep addressing `'new'`.
|
|
12
16
|
*/
|
|
13
17
|
export const useCardRouting = ({
|
|
14
18
|
itemId,
|
|
15
19
|
routeParamName = 'id',
|
|
16
|
-
parentId,
|
|
20
|
+
parentId: rawParentId,
|
|
17
21
|
manualSetup = false,
|
|
18
22
|
}: {
|
|
19
23
|
itemId: Ref<CardItemId>;
|
|
20
24
|
routeParamName?: string;
|
|
21
|
-
parentId?:
|
|
25
|
+
parentId?: MaybeRefOrGetter<CardParentId>;
|
|
22
26
|
manualSetup?: boolean;
|
|
23
27
|
}) => {
|
|
24
28
|
const router = useRouter();
|
|
25
29
|
const route = useRoute();
|
|
26
30
|
|
|
27
31
|
const routeId = computed(() => route.params[routeParamName]);
|
|
32
|
+
/** resolved on read, so a nested card always addresses the current parent */
|
|
33
|
+
const parentId = computed(() => toValue(rawParentId));
|
|
28
34
|
|
|
29
35
|
if (!manualSetup) {
|
|
30
36
|
const unwatch = watch(itemId, async (next, prev) => {
|
|
31
37
|
if (next && !prev) {
|
|
32
|
-
if (!parentId || routeId.value === 'new') {
|
|
38
|
+
if (!parentId.value || routeId.value === 'new') {
|
|
33
39
|
await router.replace({
|
|
34
40
|
params: {
|
|
35
41
|
...route.params,
|
|
@@ -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
|
import { type StoreDefinition, storeToRefs } from 'pinia';
|
|
2
|
-
import { watch } from 'vue';
|
|
2
|
+
import { type MaybeRefOrGetter, watch } from 'vue';
|
|
3
3
|
|
|
4
|
+
import type { CardParentId } from '../types/CardStore.types';
|
|
4
5
|
import { useCardComponent } from './useCardComponent';
|
|
5
6
|
import { useCardRouting } from './useCardRouting';
|
|
6
7
|
|
|
@@ -9,7 +10,9 @@ import { useCardRouting } from './useCardRouting';
|
|
|
9
10
|
*
|
|
10
11
|
* Wraps `useCardComponent` with `manualSetup: true` and delegates
|
|
11
12
|
* routing to `useCardRouting`. Pass `parentId` from the component —
|
|
12
|
-
* its presence indicates a nested card context.
|
|
13
|
+
* its presence indicates a nested card context. A ref or getter is read on
|
|
14
|
+
* every `initialize`, so a popup mounted while its parent was still `'new'`
|
|
15
|
+
* follows the parent's real id instead of addressing `'new'` forever.
|
|
13
16
|
*
|
|
14
17
|
* @example
|
|
15
18
|
* ```ts
|
|
@@ -34,7 +37,7 @@ export const useNestedCardComponent = <
|
|
|
34
37
|
useCardStore: StoreDefinition;
|
|
35
38
|
onLoadErrorHandler?: (err: unknown) => void;
|
|
36
39
|
routeParamName: string;
|
|
37
|
-
parentId?:
|
|
40
|
+
parentId?: MaybeRefOrGetter<CardParentId>;
|
|
38
41
|
}) => {
|
|
39
42
|
const cardSetup = useCardComponent<CardEntity>({
|
|
40
43
|
useCardStore,
|
|
@@ -46,7 +49,7 @@ export const useNestedCardComponent = <
|
|
|
46
49
|
const { itemId } = storeToRefs(cardStore);
|
|
47
50
|
const { initialize, $reset } = cardStore;
|
|
48
51
|
|
|
49
|
-
const { routeId } = useCardRouting({
|
|
52
|
+
const { routeId, parentId: currentParentId } = useCardRouting({
|
|
50
53
|
itemId,
|
|
51
54
|
routeParamName,
|
|
52
55
|
parentId,
|
|
@@ -58,7 +61,7 @@ export const useNestedCardComponent = <
|
|
|
58
61
|
if (value) {
|
|
59
62
|
initialize({
|
|
60
63
|
itemId: value === 'new' ? null : value,
|
|
61
|
-
parentId,
|
|
64
|
+
parentId: currentParentId.value,
|
|
62
65
|
});
|
|
63
66
|
} else {
|
|
64
67
|
$reset();
|
|
@@ -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,12 +171,25 @@ 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,
|
|
173
190
|
}: {
|
|
174
191
|
itemId?: string | number;
|
|
175
|
-
parentId?:
|
|
192
|
+
parentId?: CardParentId;
|
|
176
193
|
} = {}) => {
|
|
177
194
|
if (initialParentId) {
|
|
178
195
|
parentId.value = 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
|
+
}
|