@snack-uikit/tree 0.10.3 → 0.11.1-preview-c3fee040.0
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/CHANGELOG.md +11 -0
- package/README.md +121 -0
- package/dist/cjs/helpers/__tests__/collectEmptyNestedNodesInExpanded.spec.d.ts +1 -0
- package/dist/cjs/helpers/__tests__/collectEmptyNestedNodesInExpanded.spec.js +80 -0
- package/dist/cjs/helpers/__tests__/setChildrenOfTreeNode.spec.d.ts +1 -0
- package/dist/cjs/helpers/__tests__/setChildrenOfTreeNode.spec.js +102 -0
- package/dist/cjs/helpers/collectEmptyNestedNodesInExpanded.d.ts +2 -0
- package/dist/cjs/helpers/collectEmptyNestedNodesInExpanded.js +17 -0
- package/dist/cjs/helpers/index.d.ts +2 -0
- package/dist/cjs/helpers/index.js +3 -1
- package/dist/cjs/helpers/setChildrenOfTreeNode.d.ts +2 -0
- package/dist/cjs/helpers/setChildrenOfTreeNode.js +28 -0
- package/dist/cjs/helpers/sortTreeItemsByTitle.js +2 -2
- package/dist/cjs/helpers/traverse.d.ts +7 -1
- package/dist/cjs/helpers/traverse.js +38 -2
- package/dist/cjs/hooks/__tests__/useSearchableTree.spec.d.ts +1 -0
- package/dist/cjs/hooks/__tests__/useSearchableTree.spec.js +230 -0
- package/dist/cjs/hooks/__tests__/useTreeMultiSelection.spec.d.ts +1 -0
- package/dist/cjs/hooks/__tests__/useTreeMultiSelection.spec.js +225 -0
- package/dist/cjs/hooks/index.d.ts +2 -0
- package/dist/cjs/hooks/index.js +26 -0
- package/dist/cjs/hooks/useSearchableTree.d.ts +28 -0
- package/dist/cjs/hooks/useSearchableTree.js +147 -0
- package/dist/cjs/hooks/useTreeMultiSelection.d.ts +12 -0
- package/dist/cjs/hooks/useTreeMultiSelection.js +82 -0
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/types.d.ts +17 -0
- package/dist/esm/helpers/__tests__/collectEmptyNestedNodesInExpanded.spec.d.ts +1 -0
- package/dist/esm/helpers/__tests__/collectEmptyNestedNodesInExpanded.spec.js +64 -0
- package/dist/esm/helpers/__tests__/setChildrenOfTreeNode.spec.d.ts +1 -0
- package/dist/esm/helpers/__tests__/setChildrenOfTreeNode.spec.js +50 -0
- package/dist/esm/helpers/collectEmptyNestedNodesInExpanded.d.ts +2 -0
- package/dist/esm/helpers/collectEmptyNestedNodesInExpanded.js +11 -0
- package/dist/esm/helpers/index.d.ts +2 -0
- package/dist/esm/helpers/index.js +2 -0
- package/dist/esm/helpers/setChildrenOfTreeNode.d.ts +2 -0
- package/dist/esm/helpers/setChildrenOfTreeNode.js +20 -0
- package/dist/esm/helpers/sortTreeItemsByTitle.js +2 -2
- package/dist/esm/helpers/traverse.d.ts +7 -1
- package/dist/esm/helpers/traverse.js +24 -0
- package/dist/esm/hooks/__tests__/useSearchableTree.spec.d.ts +1 -0
- package/dist/esm/hooks/__tests__/useSearchableTree.spec.js +165 -0
- package/dist/esm/hooks/__tests__/useTreeMultiSelection.spec.d.ts +1 -0
- package/dist/esm/hooks/__tests__/useTreeMultiSelection.spec.js +130 -0
- package/dist/esm/hooks/index.d.ts +2 -0
- package/dist/esm/hooks/index.js +2 -0
- package/dist/esm/hooks/useSearchableTree.d.ts +28 -0
- package/dist/esm/hooks/useSearchableTree.js +108 -0
- package/dist/esm/hooks/useTreeMultiSelection.d.ts +12 -0
- package/dist/esm/hooks/useTreeMultiSelection.js +43 -0
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/types.d.ts +17 -0
- package/package.json +22 -4
- package/src/helpers/__tests__/collectEmptyNestedNodesInExpanded.spec.ts +88 -0
- package/src/helpers/__tests__/setChildrenOfTreeNode.spec.ts +68 -0
- package/src/helpers/collectEmptyNestedNodesInExpanded.ts +16 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/setChildrenOfTreeNode.ts +30 -0
- package/src/helpers/sortTreeItemsByTitle.ts +2 -2
- package/src/helpers/traverse.ts +38 -1
- package/src/hooks/__tests__/useSearchableTree.spec.ts +200 -0
- package/src/hooks/__tests__/useTreeMultiSelection.spec.ts +165 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useSearchableTree.ts +163 -0
- package/src/hooks/useTreeMultiSelection.ts +61 -0
- package/src/index.ts +2 -1
- package/src/types.ts +15 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) {
|
|
5
|
+
return value instanceof P ? value : new P(function (resolve) {
|
|
6
|
+
resolve(value);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
+
function fulfilled(value) {
|
|
11
|
+
try {
|
|
12
|
+
step(generator.next(value));
|
|
13
|
+
} catch (e) {
|
|
14
|
+
reject(e);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function rejected(value) {
|
|
18
|
+
try {
|
|
19
|
+
step(generator["throw"](value));
|
|
20
|
+
} catch (e) {
|
|
21
|
+
reject(e);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function step(result) {
|
|
25
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
26
|
+
}
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(exports, "__esModule", {
|
|
31
|
+
value: true
|
|
32
|
+
});
|
|
33
|
+
const react_1 = require("@testing-library/react");
|
|
34
|
+
const react_2 = require("react");
|
|
35
|
+
const vitest_1 = require("vitest");
|
|
36
|
+
const useSearchableTree_1 = require("../useSearchableTree");
|
|
37
|
+
vitest_1.vi.mock('@siberiacancode/reactuse', () => ({
|
|
38
|
+
useDebounceValue: value => value,
|
|
39
|
+
useDidUpdate: (effect, deps) => {
|
|
40
|
+
const isFirstRenderRef = (0, react_2.useRef)(true);
|
|
41
|
+
(0, react_2.useEffect)(() => {
|
|
42
|
+
if (isFirstRenderRef.current) {
|
|
43
|
+
isFirstRenderRef.current = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
effect();
|
|
47
|
+
}, [deps, effect]);
|
|
48
|
+
},
|
|
49
|
+
useRefState: initialValue => (0, react_2.useRef)(initialValue)
|
|
50
|
+
}));
|
|
51
|
+
const createLeaf = id => ({
|
|
52
|
+
id,
|
|
53
|
+
title: id
|
|
54
|
+
});
|
|
55
|
+
const createParent = (id, nested) => ({
|
|
56
|
+
id,
|
|
57
|
+
title: id,
|
|
58
|
+
nested
|
|
59
|
+
});
|
|
60
|
+
const mapNodeToRecordItem = node => ({
|
|
61
|
+
label: String(node.title)
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.describe)('useSearchableTree', () => {
|
|
64
|
+
(0, vitest_1.it)('should initialize tree and treeItemsRecord from initTree', () => {
|
|
65
|
+
const initTree = [createLeaf('node-1'), createParent('node-2', [createLeaf('node-2-1')])];
|
|
66
|
+
const {
|
|
67
|
+
result
|
|
68
|
+
} = (0, react_1.renderHook)(() => (0, useSearchableTree_1.useSearchableTree)({
|
|
69
|
+
initTree,
|
|
70
|
+
onPreloadNode: vitest_1.vi.fn(),
|
|
71
|
+
onPreloadNodes: vitest_1.vi.fn(),
|
|
72
|
+
onSearch: vitest_1.vi.fn(),
|
|
73
|
+
mapNodeToRecordItem
|
|
74
|
+
}));
|
|
75
|
+
(0, vitest_1.expect)(result.current.tree.current).toEqual(initTree);
|
|
76
|
+
(0, vitest_1.expect)(result.current.treeItemsRecord.current).toEqual({
|
|
77
|
+
'node-1': {
|
|
78
|
+
label: 'node-1'
|
|
79
|
+
},
|
|
80
|
+
'node-2': {
|
|
81
|
+
label: 'node-2'
|
|
82
|
+
},
|
|
83
|
+
'node-2-1': {
|
|
84
|
+
label: 'node-2-1'
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.it)('should update expandedNodes on onExpand call', () => {
|
|
89
|
+
const {
|
|
90
|
+
result
|
|
91
|
+
} = (0, react_1.renderHook)(() => (0, useSearchableTree_1.useSearchableTree)({
|
|
92
|
+
initTree: [createLeaf('root')],
|
|
93
|
+
onPreloadNode: vitest_1.vi.fn(),
|
|
94
|
+
onPreloadNodes: vitest_1.vi.fn(),
|
|
95
|
+
onSearch: vitest_1.vi.fn(),
|
|
96
|
+
mapNodeToRecordItem
|
|
97
|
+
}));
|
|
98
|
+
(0, react_1.act)(() => {
|
|
99
|
+
result.current.onExpand(['root', 'child']);
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.expect)(result.current.expandedNodes.current).toEqual(['root', 'child']);
|
|
102
|
+
});
|
|
103
|
+
(0, vitest_1.it)('should preload node children and update tree and record on onDataLoad', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
const initTree = [createParent('root', [])];
|
|
105
|
+
const preloadedChildren = [createLeaf('child-1'), createLeaf('child-2')];
|
|
106
|
+
const onPreloadNode = vitest_1.vi.fn().mockResolvedValue(preloadedChildren);
|
|
107
|
+
const {
|
|
108
|
+
result
|
|
109
|
+
} = (0, react_1.renderHook)(() => (0, useSearchableTree_1.useSearchableTree)({
|
|
110
|
+
initTree,
|
|
111
|
+
onPreloadNode,
|
|
112
|
+
onPreloadNodes: vitest_1.vi.fn(),
|
|
113
|
+
onSearch: vitest_1.vi.fn(),
|
|
114
|
+
mapNodeToRecordItem
|
|
115
|
+
}));
|
|
116
|
+
let loadResult;
|
|
117
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
|
+
loadResult = yield result.current.onDataLoad(createParent('root', []));
|
|
119
|
+
}));
|
|
120
|
+
(0, vitest_1.expect)(onPreloadNode).toHaveBeenCalledWith(createParent('root', []));
|
|
121
|
+
(0, vitest_1.expect)(loadResult).toEqual({
|
|
122
|
+
preloadedChildren,
|
|
123
|
+
updatedTree: [createParent('root', preloadedChildren)],
|
|
124
|
+
newTreeItemsRecord: {
|
|
125
|
+
root: {
|
|
126
|
+
label: 'root'
|
|
127
|
+
},
|
|
128
|
+
'child-1': {
|
|
129
|
+
label: 'child-1'
|
|
130
|
+
},
|
|
131
|
+
'child-2': {
|
|
132
|
+
label: 'child-2'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
(0, vitest_1.expect)(result.current.tree.current).toEqual([createParent('root', preloadedChildren)]);
|
|
137
|
+
(0, vitest_1.expect)(result.current.treeItemsRecord.current).toEqual({
|
|
138
|
+
root: {
|
|
139
|
+
label: 'root'
|
|
140
|
+
},
|
|
141
|
+
'child-1': {
|
|
142
|
+
label: 'child-1'
|
|
143
|
+
},
|
|
144
|
+
'child-2': {
|
|
145
|
+
label: 'child-2'
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}));
|
|
149
|
+
(0, vitest_1.it)('should run search and preload required nodes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
const searchedTree = [createParent('expandable-root', []), createLeaf('leaf')];
|
|
151
|
+
const onSearch = vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
152
|
+
return {
|
|
153
|
+
tree: searchedTree,
|
|
154
|
+
needPreloadNodes: ['expandable-root', 'external-root']
|
|
155
|
+
};
|
|
156
|
+
}));
|
|
157
|
+
const onPreloadNodes = vitest_1.vi.fn(nodeIds => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
|
+
return Object.assign({
|
|
159
|
+
'expandable-root': [createLeaf('expandable-child')],
|
|
160
|
+
'external-root': [createLeaf('external-child')]
|
|
161
|
+
}, Object.fromEntries(nodeIds.filter(id => id !== 'expandable-root' && id !== 'external-root').map(id => [id, []])));
|
|
162
|
+
}));
|
|
163
|
+
const {
|
|
164
|
+
result
|
|
165
|
+
} = (0, react_1.renderHook)(() => (0, useSearchableTree_1.useSearchableTree)({
|
|
166
|
+
initTree: [createParent('init-root', [])],
|
|
167
|
+
onPreloadNode: vitest_1.vi.fn(),
|
|
168
|
+
onPreloadNodes,
|
|
169
|
+
onSearch,
|
|
170
|
+
mapNodeToRecordItem
|
|
171
|
+
}));
|
|
172
|
+
(0, react_1.act)(() => {
|
|
173
|
+
result.current.onExpand(['expandable-root']);
|
|
174
|
+
result.current.search.onChange('query');
|
|
175
|
+
});
|
|
176
|
+
yield (0, react_1.waitFor)(() => {
|
|
177
|
+
(0, vitest_1.expect)(onSearch).toHaveBeenCalledWith({
|
|
178
|
+
search: 'query'
|
|
179
|
+
}, vitest_1.expect.any(AbortSignal));
|
|
180
|
+
(0, vitest_1.expect)(onPreloadNodes).toHaveBeenCalledWith(['expandable-root', 'external-root'], vitest_1.expect.any(AbortSignal));
|
|
181
|
+
});
|
|
182
|
+
(0, vitest_1.expect)(result.current.tree.current).toEqual([createParent('expandable-root', [createLeaf('expandable-child')]), createLeaf('leaf')]);
|
|
183
|
+
(0, vitest_1.expect)(result.current.treeItemsRecord.current).toEqual({
|
|
184
|
+
'expandable-root': {
|
|
185
|
+
label: 'expandable-root'
|
|
186
|
+
},
|
|
187
|
+
'expandable-child': {
|
|
188
|
+
label: 'expandable-child'
|
|
189
|
+
},
|
|
190
|
+
leaf: {
|
|
191
|
+
label: 'leaf'
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}));
|
|
195
|
+
(0, vitest_1.it)('should update tree and record from search result without preloading', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
196
|
+
const onSearch = vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
197
|
+
return {
|
|
198
|
+
tree: [createParent('root-node', [createLeaf('child-node')])],
|
|
199
|
+
needPreloadNodes: []
|
|
200
|
+
};
|
|
201
|
+
}));
|
|
202
|
+
const {
|
|
203
|
+
result
|
|
204
|
+
} = (0, react_1.renderHook)(() => (0, useSearchableTree_1.useSearchableTree)({
|
|
205
|
+
initTree: [createLeaf('initial')],
|
|
206
|
+
onPreloadNode: vitest_1.vi.fn(),
|
|
207
|
+
onPreloadNodes: vitest_1.vi.fn(),
|
|
208
|
+
onSearch,
|
|
209
|
+
mapNodeToRecordItem
|
|
210
|
+
}));
|
|
211
|
+
(0, react_1.act)(() => {
|
|
212
|
+
result.current.search.onChange('query');
|
|
213
|
+
});
|
|
214
|
+
yield (0, react_1.waitFor)(() => {
|
|
215
|
+
(0, vitest_1.expect)(onSearch).toHaveBeenCalledWith({
|
|
216
|
+
search: 'query'
|
|
217
|
+
}, vitest_1.expect.any(AbortSignal));
|
|
218
|
+
(0, vitest_1.expect)(result.current.tree.current).toEqual([createParent('root-node', [createLeaf('child-node')])]);
|
|
219
|
+
});
|
|
220
|
+
(0, vitest_1.expect)(result.current.expandedNodes.current).toEqual([]);
|
|
221
|
+
(0, vitest_1.expect)(result.current.treeItemsRecord.current).toEqual({
|
|
222
|
+
'root-node': {
|
|
223
|
+
label: 'root-node'
|
|
224
|
+
},
|
|
225
|
+
'child-node': {
|
|
226
|
+
label: 'child-node'
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}));
|
|
230
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) {
|
|
5
|
+
return value instanceof P ? value : new P(function (resolve) {
|
|
6
|
+
resolve(value);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
+
function fulfilled(value) {
|
|
11
|
+
try {
|
|
12
|
+
step(generator.next(value));
|
|
13
|
+
} catch (e) {
|
|
14
|
+
reject(e);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function rejected(value) {
|
|
18
|
+
try {
|
|
19
|
+
step(generator["throw"](value));
|
|
20
|
+
} catch (e) {
|
|
21
|
+
reject(e);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function step(result) {
|
|
25
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
26
|
+
}
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(exports, "__esModule", {
|
|
31
|
+
value: true
|
|
32
|
+
});
|
|
33
|
+
const react_1 = require("@testing-library/react");
|
|
34
|
+
const vitest_1 = require("vitest");
|
|
35
|
+
const useTreeMultiSelection_1 = require("../useTreeMultiSelection");
|
|
36
|
+
const createLeaf = id => ({
|
|
37
|
+
id,
|
|
38
|
+
title: id
|
|
39
|
+
});
|
|
40
|
+
const createParent = (id, nested) => ({
|
|
41
|
+
id,
|
|
42
|
+
title: id,
|
|
43
|
+
nested
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.describe)('useTreeMultiSelection', () => {
|
|
46
|
+
(0, vitest_1.it)('should keep selected state uncontrolled by default', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
const {
|
|
48
|
+
result
|
|
49
|
+
} = (0, react_1.renderHook)(() => (0, useTreeMultiSelection_1.useTreeMultiSelection)({
|
|
50
|
+
onDataLoad: vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
51
|
+
return {
|
|
52
|
+
preloadedChildren: [],
|
|
53
|
+
updatedTree: []
|
|
54
|
+
};
|
|
55
|
+
})),
|
|
56
|
+
onSelect: () => ({
|
|
57
|
+
added: ['a'],
|
|
58
|
+
removed: []
|
|
59
|
+
})
|
|
60
|
+
}));
|
|
61
|
+
(0, vitest_1.expect)(result.current.selected).toEqual([]);
|
|
62
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
yield result.current.onSelect(['a'], createLeaf('a'));
|
|
64
|
+
}));
|
|
65
|
+
(0, vitest_1.expect)(result.current.selected).toEqual(['a']);
|
|
66
|
+
}));
|
|
67
|
+
(0, vitest_1.it)('should call onChangeSelected when selection updates', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
68
|
+
const onChangeSelected = vitest_1.vi.fn();
|
|
69
|
+
const {
|
|
70
|
+
result
|
|
71
|
+
} = (0, react_1.renderHook)(() => (0, useTreeMultiSelection_1.useTreeMultiSelection)({
|
|
72
|
+
onDataLoad: vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
+
return {
|
|
74
|
+
preloadedChildren: [],
|
|
75
|
+
updatedTree: []
|
|
76
|
+
};
|
|
77
|
+
})),
|
|
78
|
+
onSelect: () => ({
|
|
79
|
+
added: ['a'],
|
|
80
|
+
removed: []
|
|
81
|
+
}),
|
|
82
|
+
onChangeSelected
|
|
83
|
+
}));
|
|
84
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
yield result.current.onSelect(['a'], createLeaf('a'));
|
|
86
|
+
}));
|
|
87
|
+
(0, vitest_1.expect)(onChangeSelected).toHaveBeenCalledWith(['a']);
|
|
88
|
+
}));
|
|
89
|
+
(0, vitest_1.it)('should treat selected prop as controlled', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
const onChangeSelected = vitest_1.vi.fn();
|
|
91
|
+
const {
|
|
92
|
+
result,
|
|
93
|
+
rerender
|
|
94
|
+
} = (0, react_1.renderHook)(_ref => {
|
|
95
|
+
let {
|
|
96
|
+
selected
|
|
97
|
+
} = _ref;
|
|
98
|
+
return (0, useTreeMultiSelection_1.useTreeMultiSelection)({
|
|
99
|
+
onDataLoad: vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
return {
|
|
101
|
+
preloadedChildren: [],
|
|
102
|
+
updatedTree: []
|
|
103
|
+
};
|
|
104
|
+
})),
|
|
105
|
+
onSelect: () => ({
|
|
106
|
+
added: ['b'],
|
|
107
|
+
removed: []
|
|
108
|
+
}),
|
|
109
|
+
selected,
|
|
110
|
+
onChangeSelected
|
|
111
|
+
});
|
|
112
|
+
}, {
|
|
113
|
+
initialProps: {
|
|
114
|
+
selected: ['a']
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.expect)(result.current.selected).toEqual(['a']);
|
|
118
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
yield result.current.onSelect(['a', 'b'], createLeaf('b'));
|
|
120
|
+
}));
|
|
121
|
+
// controlled value does not change until parent updates it
|
|
122
|
+
(0, vitest_1.expect)(result.current.selected).toEqual(['a']);
|
|
123
|
+
(0, vitest_1.expect)(onChangeSelected).toHaveBeenCalledWith(['a', 'b']);
|
|
124
|
+
rerender({
|
|
125
|
+
selected: ['a', 'b']
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.expect)(result.current.selected).toEqual(['a', 'b']);
|
|
128
|
+
}));
|
|
129
|
+
(0, vitest_1.it)('should preload children when selecting empty parent node and pass cloned node to onSelect', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
const node = createParent('parent', []);
|
|
131
|
+
const preloadedChildren = [createLeaf('child-1'), createLeaf('child-2')];
|
|
132
|
+
const onDataLoad = vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
|
+
return {
|
|
134
|
+
preloadedChildren,
|
|
135
|
+
updatedTree: [createParent('parent', preloadedChildren)]
|
|
136
|
+
};
|
|
137
|
+
}));
|
|
138
|
+
const onSelect = vitest_1.vi.fn(() => ({
|
|
139
|
+
added: ['parent', 'child-1', 'child-2'],
|
|
140
|
+
removed: []
|
|
141
|
+
}));
|
|
142
|
+
const {
|
|
143
|
+
result
|
|
144
|
+
} = (0, react_1.renderHook)(() => (0, useTreeMultiSelection_1.useTreeMultiSelection)({
|
|
145
|
+
onDataLoad,
|
|
146
|
+
onSelect
|
|
147
|
+
}));
|
|
148
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
149
|
+
yield result.current.onSelect(['parent'], node);
|
|
150
|
+
}));
|
|
151
|
+
(0, vitest_1.expect)(onDataLoad).toHaveBeenCalledWith(node);
|
|
152
|
+
(0, vitest_1.expect)(onSelect).toHaveBeenCalledWith({
|
|
153
|
+
selectedKeys: ['parent'],
|
|
154
|
+
node: createParent('parent', preloadedChildren),
|
|
155
|
+
isSelected: true
|
|
156
|
+
});
|
|
157
|
+
(0, vitest_1.expect)(result.current.selected.sort()).toEqual(['child-1', 'child-2', 'parent']);
|
|
158
|
+
}));
|
|
159
|
+
(0, vitest_1.it)('should not preload when parent node already has children', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
const node = createParent('parent', [createLeaf('child')]);
|
|
161
|
+
const onDataLoad = vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
162
|
+
return {
|
|
163
|
+
preloadedChildren: [],
|
|
164
|
+
updatedTree: []
|
|
165
|
+
};
|
|
166
|
+
}));
|
|
167
|
+
const onSelect = vitest_1.vi.fn(() => ({
|
|
168
|
+
added: ['parent'],
|
|
169
|
+
removed: []
|
|
170
|
+
}));
|
|
171
|
+
const {
|
|
172
|
+
result
|
|
173
|
+
} = (0, react_1.renderHook)(() => (0, useTreeMultiSelection_1.useTreeMultiSelection)({
|
|
174
|
+
onDataLoad,
|
|
175
|
+
onSelect
|
|
176
|
+
}));
|
|
177
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
yield result.current.onSelect(['parent'], node);
|
|
179
|
+
}));
|
|
180
|
+
(0, vitest_1.expect)(onDataLoad).not.toHaveBeenCalled();
|
|
181
|
+
(0, vitest_1.expect)(onSelect).toHaveBeenCalledWith({
|
|
182
|
+
selectedKeys: ['parent'],
|
|
183
|
+
node,
|
|
184
|
+
isSelected: true
|
|
185
|
+
});
|
|
186
|
+
}));
|
|
187
|
+
(0, vitest_1.it)('should add and remove ids from selection without duplicates', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
|
+
const {
|
|
189
|
+
result
|
|
190
|
+
} = (0, react_1.renderHook)(() => (0, useTreeMultiSelection_1.useTreeMultiSelection)({
|
|
191
|
+
onDataLoad: vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
192
|
+
return {
|
|
193
|
+
preloadedChildren: [],
|
|
194
|
+
updatedTree: []
|
|
195
|
+
};
|
|
196
|
+
})),
|
|
197
|
+
onSelect: () => ({
|
|
198
|
+
added: ['a', 'a', 'b'],
|
|
199
|
+
removed: []
|
|
200
|
+
})
|
|
201
|
+
}));
|
|
202
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
203
|
+
yield result.current.onSelect(['a'], createLeaf('a'));
|
|
204
|
+
}));
|
|
205
|
+
(0, vitest_1.expect)(result.current.selected.sort()).toEqual(['a', 'b']);
|
|
206
|
+
const {
|
|
207
|
+
result: result2
|
|
208
|
+
} = (0, react_1.renderHook)(() => (0, useTreeMultiSelection_1.useTreeMultiSelection)({
|
|
209
|
+
onDataLoad: vitest_1.vi.fn(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
210
|
+
return {
|
|
211
|
+
preloadedChildren: [],
|
|
212
|
+
updatedTree: []
|
|
213
|
+
};
|
|
214
|
+
})),
|
|
215
|
+
onSelect: () => ({
|
|
216
|
+
added: [],
|
|
217
|
+
removed: ['a']
|
|
218
|
+
}),
|
|
219
|
+
selected: ['a', 'b']
|
|
220
|
+
}));
|
|
221
|
+
yield (0, react_1.act)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
222
|
+
yield result2.current.onSelect(['b'], createLeaf('a'));
|
|
223
|
+
}));
|
|
224
|
+
}));
|
|
225
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var __createBinding = void 0 && (void 0).__createBinding || (Object.create ? function (o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
get: function () {
|
|
10
|
+
return m[k];
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
} : function (o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
});
|
|
19
|
+
var __exportStar = void 0 && (void 0).__exportStar || function (m, exports) {
|
|
20
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", {
|
|
23
|
+
value: true
|
|
24
|
+
});
|
|
25
|
+
__exportStar(require("./useSearchableTree"), exports);
|
|
26
|
+
__exportStar(require("./useTreeMultiSelection"), exports);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SearchableTreeDataLoadResult, TreeNodeProps } from '../types';
|
|
2
|
+
export type SearchResult<TTreeNode extends TreeNodeProps> = {
|
|
3
|
+
tree: TTreeNode[];
|
|
4
|
+
needPreloadNodes: string[];
|
|
5
|
+
};
|
|
6
|
+
export type SearchParams = {
|
|
7
|
+
search: string;
|
|
8
|
+
};
|
|
9
|
+
type UseSearchableTreeParams<TRecordValue, TTreeNode extends TreeNodeProps> = {
|
|
10
|
+
initTree: TTreeNode[];
|
|
11
|
+
onPreloadNode: (node: TreeNodeProps) => Promise<TTreeNode[]>;
|
|
12
|
+
onPreloadNodes: (nodes: string[], signal?: AbortSignal) => Promise<Record<string, TTreeNode[]>>;
|
|
13
|
+
onSearch: (params: SearchParams, signal?: AbortSignal) => Promise<SearchResult<TTreeNode>>;
|
|
14
|
+
mapNodeToRecordItem: (node: TTreeNode) => TRecordValue;
|
|
15
|
+
};
|
|
16
|
+
export declare function useSearchableTree<TRecordValue, TTreeNode extends TreeNodeProps>({ initTree, onPreloadNode, onPreloadNodes, onSearch, mapNodeToRecordItem, }: UseSearchableTreeParams<TRecordValue, TTreeNode>): {
|
|
17
|
+
tree: import("@siberiacancode/reactuse").StateRef<TTreeNode[]>;
|
|
18
|
+
expandedNodes: import("@siberiacancode/reactuse").StateRef<string[]>;
|
|
19
|
+
loading: boolean;
|
|
20
|
+
treeItemsRecord: import("@siberiacancode/reactuse").StateRef<Record<string, TRecordValue>>;
|
|
21
|
+
search: {
|
|
22
|
+
value: string;
|
|
23
|
+
onChange: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
24
|
+
};
|
|
25
|
+
onExpand: (nodes: string[]) => void;
|
|
26
|
+
onDataLoad: (node: TreeNodeProps) => Promise<SearchableTreeDataLoadResult<TTreeNode, TRecordValue>>;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) {
|
|
5
|
+
return value instanceof P ? value : new P(function (resolve) {
|
|
6
|
+
resolve(value);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
+
function fulfilled(value) {
|
|
11
|
+
try {
|
|
12
|
+
step(generator.next(value));
|
|
13
|
+
} catch (e) {
|
|
14
|
+
reject(e);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function rejected(value) {
|
|
18
|
+
try {
|
|
19
|
+
step(generator["throw"](value));
|
|
20
|
+
} catch (e) {
|
|
21
|
+
reject(e);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function step(result) {
|
|
25
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
26
|
+
}
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(exports, "__esModule", {
|
|
31
|
+
value: true
|
|
32
|
+
});
|
|
33
|
+
exports.useSearchableTree = useSearchableTree;
|
|
34
|
+
const reactuse_1 = require("@siberiacancode/reactuse");
|
|
35
|
+
const cancelable_promise_1 = require("cancelable-promise");
|
|
36
|
+
const react_1 = require("react");
|
|
37
|
+
const helpers_1 = require("../helpers");
|
|
38
|
+
function useSearchableTree(_ref) {
|
|
39
|
+
let {
|
|
40
|
+
initTree,
|
|
41
|
+
onPreloadNode,
|
|
42
|
+
onPreloadNodes,
|
|
43
|
+
onSearch,
|
|
44
|
+
mapNodeToRecordItem
|
|
45
|
+
} = _ref;
|
|
46
|
+
const tree = (0, reactuse_1.useRefState)(initTree);
|
|
47
|
+
const treeItemsRecord = (0, reactuse_1.useRefState)({});
|
|
48
|
+
const expandedNodes = (0, reactuse_1.useRefState)([]);
|
|
49
|
+
const [search, setSearch] = (0, react_1.useState)('');
|
|
50
|
+
const debouncedSearch = (0, reactuse_1.useDebounceValue)(search, 500);
|
|
51
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
52
|
+
const searchPromiseRef = (0, react_1.useRef)(null);
|
|
53
|
+
const searchAbortControllerRef = (0, react_1.useRef)(null);
|
|
54
|
+
const buildTreeItemsRecord = (0, react_1.useCallback)(nodes => {
|
|
55
|
+
const record = {};
|
|
56
|
+
(0, helpers_1.traverse)(nodes, node => {
|
|
57
|
+
record[node.id] = mapNodeToRecordItem(node);
|
|
58
|
+
});
|
|
59
|
+
return record;
|
|
60
|
+
}, [mapNodeToRecordItem]);
|
|
61
|
+
const onExpand = (0, react_1.useCallback)(nodes => {
|
|
62
|
+
expandedNodes.current = nodes;
|
|
63
|
+
}, [expandedNodes]);
|
|
64
|
+
const onDataLoad = (0, react_1.useCallback)(node => __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
const preloadedChildren = yield onPreloadNode(node);
|
|
66
|
+
const updatedTree = (0, helpers_1.setChildrenOfTreeNode)(tree.current, node.id, preloadedChildren);
|
|
67
|
+
tree.current = updatedTree;
|
|
68
|
+
const newTreeItemsRecord = Object.assign({}, treeItemsRecord.current);
|
|
69
|
+
(0, helpers_1.traverse)(preloadedChildren, child => {
|
|
70
|
+
newTreeItemsRecord[child.id] = mapNodeToRecordItem(child);
|
|
71
|
+
});
|
|
72
|
+
treeItemsRecord.current = newTreeItemsRecord;
|
|
73
|
+
return {
|
|
74
|
+
preloadedChildren,
|
|
75
|
+
updatedTree,
|
|
76
|
+
newTreeItemsRecord
|
|
77
|
+
};
|
|
78
|
+
}), [mapNodeToRecordItem, onPreloadNode, tree, treeItemsRecord]);
|
|
79
|
+
const handleSearch = (0, react_1.useCallback)(searchQuery => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
(_a = searchPromiseRef.current) === null || _a === void 0 ? void 0 : _a.cancel();
|
|
82
|
+
(_b = searchAbortControllerRef.current) === null || _b === void 0 ? void 0 : _b.abort();
|
|
83
|
+
setLoading(true);
|
|
84
|
+
const abortController = new AbortController();
|
|
85
|
+
searchAbortControllerRef.current = abortController;
|
|
86
|
+
const searchPromise = (0, cancelable_promise_1.cancelable)(onSearch({
|
|
87
|
+
search: searchQuery
|
|
88
|
+
}, abortController.signal));
|
|
89
|
+
searchPromiseRef.current = searchPromise;
|
|
90
|
+
try {
|
|
91
|
+
const {
|
|
92
|
+
tree: searchedTree,
|
|
93
|
+
needPreloadNodes
|
|
94
|
+
} = yield searchPromise;
|
|
95
|
+
if (!searchPromise.isCanceled()) {
|
|
96
|
+
tree.current = searchedTree;
|
|
97
|
+
treeItemsRecord.current = buildTreeItemsRecord(searchedTree);
|
|
98
|
+
const expandedSet = new Set(expandedNodes.current);
|
|
99
|
+
const toPreloadExpandableNodes = (0, helpers_1.collectEmptyNestedNodesInExpanded)(searchedTree, expandedSet);
|
|
100
|
+
const collectedNodesForPreload = Array.from(new Set([...toPreloadExpandableNodes.map(node => node.id), ...needPreloadNodes]));
|
|
101
|
+
if (!collectedNodesForPreload.length) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const preloadedNodes = yield onPreloadNodes(collectedNodesForPreload, abortController.signal);
|
|
105
|
+
if (searchPromiseRef.current !== searchPromise) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let tmpTree = [...searchedTree];
|
|
109
|
+
for (const [nodeId, children] of Object.entries(preloadedNodes)) {
|
|
110
|
+
tmpTree = (0, helpers_1.setChildrenOfTreeNode)(tmpTree, nodeId, children);
|
|
111
|
+
}
|
|
112
|
+
tree.current = tmpTree;
|
|
113
|
+
treeItemsRecord.current = buildTreeItemsRecord(tmpTree);
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
if (searchPromiseRef.current === searchPromise) {
|
|
117
|
+
setLoading(false);
|
|
118
|
+
searchPromiseRef.current = null;
|
|
119
|
+
searchAbortControllerRef.current = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}), [buildTreeItemsRecord, expandedNodes, onPreloadNodes, onSearch, tree, treeItemsRecord]);
|
|
123
|
+
(0, reactuse_1.useDidUpdate)(() => {
|
|
124
|
+
handleSearch(debouncedSearch);
|
|
125
|
+
}, [debouncedSearch, handleSearch]);
|
|
126
|
+
(0, react_1.useEffect)(() => {
|
|
127
|
+
tree.current = initTree;
|
|
128
|
+
treeItemsRecord.current = buildTreeItemsRecord(initTree);
|
|
129
|
+
}, [buildTreeItemsRecord, initTree, tree, treeItemsRecord]);
|
|
130
|
+
(0, react_1.useEffect)(() => () => {
|
|
131
|
+
var _a, _b;
|
|
132
|
+
(_a = searchPromiseRef.current) === null || _a === void 0 ? void 0 : _a.cancel();
|
|
133
|
+
(_b = searchAbortControllerRef.current) === null || _b === void 0 ? void 0 : _b.abort();
|
|
134
|
+
}, []);
|
|
135
|
+
return {
|
|
136
|
+
tree,
|
|
137
|
+
expandedNodes,
|
|
138
|
+
loading,
|
|
139
|
+
treeItemsRecord,
|
|
140
|
+
search: {
|
|
141
|
+
value: search,
|
|
142
|
+
onChange: setSearch
|
|
143
|
+
},
|
|
144
|
+
onExpand,
|
|
145
|
+
onDataLoad
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PreloadNodeHandler, SelectHandler, TreeNodeProps } from '../types';
|
|
2
|
+
type UseTreeMultiSelectionParams<TTreeNode extends TreeNodeProps> = {
|
|
3
|
+
onDataLoad: PreloadNodeHandler<TTreeNode>;
|
|
4
|
+
onSelect: SelectHandler;
|
|
5
|
+
selected?: string[];
|
|
6
|
+
onChangeSelected?: (newSelected: string[]) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function useTreeMultiSelection<TTreeNode extends TreeNodeProps>({ onDataLoad, onSelect: onSelectProp, selected, onChangeSelected, }: UseTreeMultiSelectionParams<TTreeNode>): {
|
|
9
|
+
selected: string[];
|
|
10
|
+
onSelect: (selectedKeys: string[], node: TreeNodeProps) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
export {};
|