@snack-uikit/tree 0.11.0 → 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/README.md +12 -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/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/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/types.ts +15 -0
|
@@ -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 {};
|
|
@@ -0,0 +1,82 @@
|
|
|
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.useTreeMultiSelection = useTreeMultiSelection;
|
|
34
|
+
const react_1 = require("react");
|
|
35
|
+
const utils_1 = require("@snack-uikit/utils");
|
|
36
|
+
const getNewSelectedIds = (selectedIds, added, removed) => {
|
|
37
|
+
const result = new Set(selectedIds);
|
|
38
|
+
added.forEach(id => {
|
|
39
|
+
result.add(id);
|
|
40
|
+
});
|
|
41
|
+
removed.forEach(id => {
|
|
42
|
+
result.delete(id);
|
|
43
|
+
});
|
|
44
|
+
return Array.from(result);
|
|
45
|
+
};
|
|
46
|
+
function useTreeMultiSelection(_ref) {
|
|
47
|
+
let {
|
|
48
|
+
onDataLoad,
|
|
49
|
+
onSelect: onSelectProp,
|
|
50
|
+
selected,
|
|
51
|
+
onChangeSelected
|
|
52
|
+
} = _ref;
|
|
53
|
+
const [selectedIds = [], setSelectedIds] = (0, utils_1.useValueControl)({
|
|
54
|
+
value: selected,
|
|
55
|
+
defaultValue: [],
|
|
56
|
+
onChange: onChangeSelected
|
|
57
|
+
});
|
|
58
|
+
const onSelect = (0, react_1.useCallback)((selectedKeys, node) => __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
const isSelected = selectedKeys.includes(node.id);
|
|
60
|
+
const clonedNode = structuredClone(node);
|
|
61
|
+
if (node.nested && !node.nested.length) {
|
|
62
|
+
const {
|
|
63
|
+
preloadedChildren
|
|
64
|
+
} = yield onDataLoad(node);
|
|
65
|
+
clonedNode.nested = preloadedChildren;
|
|
66
|
+
}
|
|
67
|
+
const {
|
|
68
|
+
added,
|
|
69
|
+
removed
|
|
70
|
+
} = onSelectProp({
|
|
71
|
+
selectedKeys,
|
|
72
|
+
node: clonedNode,
|
|
73
|
+
isSelected
|
|
74
|
+
});
|
|
75
|
+
const updatedSelectedIds = getNewSelectedIds(selectedIds, added, removed);
|
|
76
|
+
setSelectedIds(updatedSelectedIds);
|
|
77
|
+
}), [onDataLoad, onSelectProp, selectedIds, setSelectedIds]);
|
|
78
|
+
return {
|
|
79
|
+
selected: selectedIds,
|
|
80
|
+
onSelect
|
|
81
|
+
};
|
|
82
|
+
}
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -103,3 +103,20 @@ export type TreeBaseProps = TreeView | TreeMultiSelect | TreeSingleSelect;
|
|
|
103
103
|
export type ExtendedTreeNodeProps = TreeNodeProps & {
|
|
104
104
|
getTitle?(): void;
|
|
105
105
|
};
|
|
106
|
+
export type PreloadNodeHandler<TTreeNode extends TreeNodeProps> = (node: TreeNodeProps) => Promise<{
|
|
107
|
+
preloadedChildren: TTreeNode[];
|
|
108
|
+
updatedTree: TTreeNode[];
|
|
109
|
+
}>;
|
|
110
|
+
export type SearchableTreeDataLoadResult<TTreeNode extends TreeNodeProps, TRecordValue> = {
|
|
111
|
+
preloadedChildren: TTreeNode[];
|
|
112
|
+
updatedTree: TTreeNode[];
|
|
113
|
+
newTreeItemsRecord: Record<string, TRecordValue>;
|
|
114
|
+
};
|
|
115
|
+
export type SelectHandler = (props: {
|
|
116
|
+
selectedKeys: string[];
|
|
117
|
+
node: TreeNodeProps;
|
|
118
|
+
isSelected: boolean;
|
|
119
|
+
}) => {
|
|
120
|
+
added: string[];
|
|
121
|
+
removed: string[];
|
|
122
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { collectEmptyNestedNodesInExpanded } from '../collectEmptyNestedNodesInExpanded';
|
|
3
|
+
const node = (id, nested) => (Object.assign({ id, title: id }, (nested !== undefined && { nested })));
|
|
4
|
+
describe('collectEmptyNestedNodesInExpanded', () => {
|
|
5
|
+
it('returns nodes with empty nested array that are in expandedIds', () => {
|
|
6
|
+
const tree = [node('a', []), node('b', [node('b1')])];
|
|
7
|
+
const expandedIds = new Set(['a']);
|
|
8
|
+
const result = collectEmptyNestedNodesInExpanded(tree, expandedIds);
|
|
9
|
+
expect(result).toHaveLength(1);
|
|
10
|
+
expect(result[0]).toMatchObject({ id: 'a', nested: [] });
|
|
11
|
+
});
|
|
12
|
+
it('does not return nodes with empty nested if not in expandedIds', () => {
|
|
13
|
+
const tree = [node('a', []), node('b', [])];
|
|
14
|
+
const expandedIds = new Set(['b']);
|
|
15
|
+
const result = collectEmptyNestedNodesInExpanded(tree, expandedIds);
|
|
16
|
+
expect(result).toHaveLength(1);
|
|
17
|
+
expect(result[0].id).toBe('b');
|
|
18
|
+
});
|
|
19
|
+
it('does not return nodes that have non-empty nested', () => {
|
|
20
|
+
const tree = [node('a', [node('a1')]), node('b', [])];
|
|
21
|
+
const expandedIds = new Set(['a', 'b']);
|
|
22
|
+
const result = collectEmptyNestedNodesInExpanded(tree, expandedIds);
|
|
23
|
+
expect(result).toHaveLength(1);
|
|
24
|
+
expect(result[0].id).toBe('b');
|
|
25
|
+
});
|
|
26
|
+
it('does not return nodes without nested property (leaf)', () => {
|
|
27
|
+
const tree = [node('leaf'), node('empty', [])];
|
|
28
|
+
const expandedIds = new Set(['leaf', 'empty']);
|
|
29
|
+
const result = collectEmptyNestedNodesInExpanded(tree, expandedIds);
|
|
30
|
+
expect(result).toHaveLength(1);
|
|
31
|
+
expect(result[0].id).toBe('empty');
|
|
32
|
+
});
|
|
33
|
+
it('returns empty array for empty tree', () => {
|
|
34
|
+
const result = collectEmptyNestedNodesInExpanded([], new Set(['any']));
|
|
35
|
+
expect(result).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
it('returns empty array when expandedIds is empty', () => {
|
|
38
|
+
const tree = [node('a', []), node('b', [])];
|
|
39
|
+
const result = collectEmptyNestedNodesInExpanded(tree, new Set());
|
|
40
|
+
expect(result).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
it('collects from deeply nested nodes', () => {
|
|
43
|
+
const child = node('child', []);
|
|
44
|
+
const tree = [node('root', [node('mid', [child])])];
|
|
45
|
+
const expandedIds = new Set(['root', 'mid', 'child']);
|
|
46
|
+
const result = collectEmptyNestedNodesInExpanded(tree, expandedIds);
|
|
47
|
+
expect(result).toHaveLength(1);
|
|
48
|
+
expect(result[0]).toMatchObject({ id: 'child', nested: [] });
|
|
49
|
+
});
|
|
50
|
+
it('returns all matching nodes from different levels', () => {
|
|
51
|
+
const tree = [node('a', []), node('b', [node('b1', []), node('b2', [])])];
|
|
52
|
+
const expandedIds = new Set(['a', 'b', 'b1', 'b2']);
|
|
53
|
+
const result = collectEmptyNestedNodesInExpanded(tree, expandedIds);
|
|
54
|
+
expect(result).toHaveLength(3);
|
|
55
|
+
expect(result.map(n => n.id).sort()).toEqual(['a', 'b1', 'b2']);
|
|
56
|
+
});
|
|
57
|
+
it('only includes node if both empty nested and expanded', () => {
|
|
58
|
+
const tree = [node('expanded', []), node('collapsed', [])];
|
|
59
|
+
const expandedIds = new Set(['expanded']);
|
|
60
|
+
const result = collectEmptyNestedNodesInExpanded(tree, expandedIds);
|
|
61
|
+
expect(result).toHaveLength(1);
|
|
62
|
+
expect(result[0].id).toBe('expanded');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { setChildrenOfTreeNode } from '../setChildrenOfTreeNode';
|
|
3
|
+
const node = (id, nested) => (Object.assign({ id, title: id }, (nested !== undefined && { nested: nested.map(n => (Object.assign(Object.assign({}, n), { title: n.id }))) })));
|
|
4
|
+
describe('setChildrenOfTreeNode', () => {
|
|
5
|
+
it('replaces nested for root-level node', () => {
|
|
6
|
+
const tree = [node('a', [{ id: 'a1', title: 'a1' }]), node('b', [{ id: 'b1', title: 'b1' }])];
|
|
7
|
+
const children = [node('new1'), node('new2')];
|
|
8
|
+
const result = setChildrenOfTreeNode(tree, 'b', children);
|
|
9
|
+
expect(result).toHaveLength(2);
|
|
10
|
+
expect(result[0]).toMatchObject({ id: 'a', nested: [{ id: 'a1' }] });
|
|
11
|
+
expect(result[1]).toMatchObject({ id: 'b', nested: [{ id: 'new1' }, { id: 'new2' }] });
|
|
12
|
+
});
|
|
13
|
+
it('replaces nested for deeply nested node', () => {
|
|
14
|
+
const childWithNested = node('child');
|
|
15
|
+
childWithNested.nested = [{ id: 'grand', title: 'grand' }];
|
|
16
|
+
const tree = [node('root', [childWithNested])];
|
|
17
|
+
const children = [node('replacement')];
|
|
18
|
+
const result = setChildrenOfTreeNode(tree, 'child', children);
|
|
19
|
+
const root = result[0];
|
|
20
|
+
expect(root.nested).toHaveLength(1);
|
|
21
|
+
expect(root.nested[0]).toMatchObject({ id: 'child', nested: [{ id: 'replacement' }] });
|
|
22
|
+
});
|
|
23
|
+
it('returns cloned tree when node is not found', () => {
|
|
24
|
+
const tree = [node('a'), node('b')];
|
|
25
|
+
const result = setChildrenOfTreeNode(tree, 'missing', [node('x')]);
|
|
26
|
+
expect(result).toHaveLength(2);
|
|
27
|
+
expect(result[0]).toMatchObject({ id: 'a' });
|
|
28
|
+
expect(result[1]).toMatchObject({ id: 'b' });
|
|
29
|
+
expect(result).not.toBe(tree);
|
|
30
|
+
});
|
|
31
|
+
it('returns empty array for empty tree', () => {
|
|
32
|
+
const result = setChildrenOfTreeNode([], 'any', [node('x')]);
|
|
33
|
+
expect(result).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it('sets children for target node that had no nested (leaf)', () => {
|
|
36
|
+
const tree = [node('leaf'), node('other')];
|
|
37
|
+
const children = [node('new1')];
|
|
38
|
+
const result = setChildrenOfTreeNode(tree, 'leaf', children);
|
|
39
|
+
expect(result[0]).toMatchObject({ id: 'leaf', nested: [{ id: 'new1' }] });
|
|
40
|
+
expect(result[1]).toMatchObject({ id: 'other' });
|
|
41
|
+
});
|
|
42
|
+
it('preserves order of root nodes when target is second', () => {
|
|
43
|
+
const tree = [node('first'), node('second', [{ id: 's1', title: 's1' }]), node('third')];
|
|
44
|
+
const children = [node('new1'), node('new2')];
|
|
45
|
+
const result = setChildrenOfTreeNode(tree, 'second', children);
|
|
46
|
+
expect(result.map(n => n.id)).toEqual(['first', 'second', 'third']);
|
|
47
|
+
expect(result[1].nested).toHaveLength(2);
|
|
48
|
+
expect(result[1].nested.map(n => n.id)).toEqual(['new1', 'new2']);
|
|
49
|
+
});
|
|
50
|
+
});
|