@onehat/ui 0.2.74 → 0.2.76
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/Components/Buttons/IconButton.js +7 -2
- package/src/Components/Hoc/withAlert.js +42 -39
- package/src/Components/Hoc/withEditor.js +136 -59
- package/src/Components/Hoc/withSideEditor.js +2 -2
- package/src/Components/Hoc/withWindowedEditor.js +2 -2
- package/src/Components/Tree/Tree.js +389 -315
- package/src/Constants/Styles.js +1 -1
- package/src/Functions/getIconButtonFromConfig.js +8 -9
|
@@ -52,18 +52,6 @@ import Toolbar from '../Toolbar/Toolbar.js';
|
|
|
52
52
|
import _ from 'lodash';
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
//////////////////////
|
|
56
|
-
//////////////////////
|
|
57
|
-
|
|
58
|
-
// Need to take into account whether using Repository or data.
|
|
59
|
-
// If using data, everything exists at once. What format will data be in?
|
|
60
|
-
// How does this interface with Repository?
|
|
61
|
-
// Maybe if Repository is not AjaxRepository, everything needs to be present at once!
|
|
62
|
-
|
|
63
|
-
//////////////////////
|
|
64
|
-
//////////////////////
|
|
65
|
-
|
|
66
|
-
|
|
67
55
|
function TreeComponent(props) {
|
|
68
56
|
const {
|
|
69
57
|
areRootsVisible = true,
|
|
@@ -103,7 +91,7 @@ function TreeComponent(props) {
|
|
|
103
91
|
additionalToolbarButtons = [],
|
|
104
92
|
reload = null, // Whenever this value changes after initial render, the tree will reload from scratch
|
|
105
93
|
parentIdIx,
|
|
106
|
-
|
|
94
|
+
|
|
107
95
|
// withEditor
|
|
108
96
|
onAdd,
|
|
109
97
|
onEdit,
|
|
@@ -112,6 +100,7 @@ function TreeComponent(props) {
|
|
|
112
100
|
onDuplicate,
|
|
113
101
|
onReset,
|
|
114
102
|
onContextMenu,
|
|
103
|
+
setWithEditListeners,
|
|
115
104
|
|
|
116
105
|
// withData
|
|
117
106
|
Repository,
|
|
@@ -141,16 +130,27 @@ function TreeComponent(props) {
|
|
|
141
130
|
styles = UiGlobals.styles,
|
|
142
131
|
forceUpdate = useForceUpdate(),
|
|
143
132
|
treeRef = useRef(),
|
|
133
|
+
treeNodeData = useRef(),
|
|
144
134
|
[isReady, setIsReady] = useState(false),
|
|
145
135
|
[isLoading, setIsLoading] = useState(false),
|
|
146
136
|
[isReorderMode, setIsReorderMode] = useState(false),
|
|
147
137
|
[isSearchModalShown, setIsSearchModalShown] = useState(false),
|
|
148
|
-
[treeNodeData, setTreeNodeData] = useState({}),
|
|
149
138
|
[searchResults, setSearchResults] = useState([]),
|
|
150
139
|
[searchFormData, setSearchFormData] = useState([]),
|
|
151
140
|
[dragNodeSlot, setDragNodeSlot] = useState(null),
|
|
152
141
|
[dragNodeIx, setDragNodeIx] = useState(),
|
|
153
142
|
[treeSearchValue, setTreeSearchValue] = useState(''),
|
|
143
|
+
|
|
144
|
+
// state getters & setters
|
|
145
|
+
getTreeNodeData = () => {
|
|
146
|
+
return treeNodeData.current;
|
|
147
|
+
},
|
|
148
|
+
setTreeNodeData = (tnd) => {
|
|
149
|
+
treeNodeData.current = tnd;
|
|
150
|
+
forceUpdate();
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// event handers
|
|
154
154
|
onNodeClick = (item, e) => {
|
|
155
155
|
if (!setSelection) {
|
|
156
156
|
return;
|
|
@@ -213,65 +213,137 @@ function TreeComponent(props) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
},
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
onBeforeAdd = async () => {
|
|
217
|
+
// Load children before adding the new node
|
|
218
|
+
const
|
|
219
|
+
parent = selection[0],
|
|
220
|
+
parentDatum = getNodeData(parent.id);
|
|
221
|
+
|
|
222
|
+
if (parent.hasChildren && !parent.areChildrenLoaded) {
|
|
223
|
+
await loadChildren(parentDatum);
|
|
219
224
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
},
|
|
226
|
+
onAfterAdd = async (entity) => {
|
|
227
|
+
// Expand the parent before showing the new node
|
|
228
|
+
const
|
|
229
|
+
parent = entity.parent,
|
|
230
|
+
parentDatum = getNodeData(parent.id);
|
|
231
|
+
|
|
232
|
+
if (!parentDatum.isExpanded) {
|
|
233
|
+
parentDatum.isExpanded = true;
|
|
226
234
|
}
|
|
235
|
+
|
|
236
|
+
// Add the entity to the tree
|
|
237
|
+
const entityDatum = buildTreeNodeDatum(entity);
|
|
238
|
+
parentDatum.children.unshift(entityDatum);
|
|
239
|
+
forceUpdate();
|
|
227
240
|
},
|
|
228
|
-
|
|
241
|
+
onBeforeEditSave = (entities) => {
|
|
242
|
+
onBeforeSave(entities);
|
|
243
|
+
},
|
|
244
|
+
onAfterEdit = async (entities) => {
|
|
245
|
+
// Refresh the node's display
|
|
229
246
|
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
node = entities[0],
|
|
248
|
+
existingDatum = getNodeData(node.id), // TODO: Make this work for >1 entity
|
|
249
|
+
newDatum = buildTreeNodeDatum(node);
|
|
250
|
+
|
|
251
|
+
// copy the updated data to existingDatum
|
|
252
|
+
_.merge(existingDatum, newDatum);
|
|
253
|
+
existingDatum.isLoading = false;
|
|
254
|
+
forceUpdate();
|
|
255
|
+
},
|
|
256
|
+
onBeforeDeleteSave = (entities) => {
|
|
257
|
+
onBeforeSave(entities);
|
|
258
|
+
},
|
|
259
|
+
onBeforeSave = (entities) => {
|
|
260
|
+
const
|
|
261
|
+
node = entities[0],
|
|
262
|
+
datum = getNodeData(node.id); // TODO: Make this work for >1 entity
|
|
263
|
+
|
|
264
|
+
datum.isLoading = true;
|
|
265
|
+
forceUpdate();
|
|
266
|
+
},
|
|
267
|
+
onAfterDelete = async (entities) => {
|
|
268
|
+
// TODO: Refresh the parent node
|
|
269
|
+
|
|
270
|
+
debugger;
|
|
271
|
+
},
|
|
272
|
+
onToggle = (datum) => {
|
|
273
|
+
if (datum.isLoading) {
|
|
274
|
+
return;
|
|
254
275
|
}
|
|
255
|
-
const items = _.map(buttons, getIconButtonFromConfig);
|
|
256
276
|
|
|
257
|
-
|
|
258
|
-
key="searchNodes"
|
|
259
|
-
flex={1}
|
|
260
|
-
placeholder="Find tree node"
|
|
261
|
-
onChangeText={(val) => setTreeSearchValue(val)}
|
|
262
|
-
onKeyPress={(e, value) => {
|
|
263
|
-
if (e.key === 'Enter') {
|
|
264
|
-
onSearchTree(value);
|
|
265
|
-
}
|
|
266
|
-
}}
|
|
267
|
-
value={treeSearchValue}
|
|
268
|
-
autoSubmit={false}
|
|
269
|
-
/>);
|
|
277
|
+
datum.isExpanded = !datum.isExpanded;
|
|
270
278
|
|
|
271
|
-
|
|
279
|
+
if (datum.isExpanded && datum.item.repository?.isRemote && datum.item.hasChildren && !datum.item.areChildrenLoaded) {
|
|
280
|
+
loadChildren(datum, 1);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!datum.isExpanded && datumContainsSelection(datum)) {
|
|
285
|
+
deselectAll();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
forceUpdate();
|
|
272
289
|
},
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
onCollapseAll = (setNewTreeNodeData = true) => {
|
|
291
|
+
// Go through whole tree and collapse all nodes
|
|
292
|
+
const newTreeNodeData = _.clone(getTreeNodeData());
|
|
293
|
+
collapseNodes(newTreeNodeData);
|
|
294
|
+
|
|
295
|
+
if (setNewTreeNodeData) {
|
|
296
|
+
setTreeNodeData(newTreeNodeData);
|
|
297
|
+
}
|
|
298
|
+
return newTreeNodeData;
|
|
299
|
+
},
|
|
300
|
+
onSearchTree = async (value) => {
|
|
301
|
+
let found = [];
|
|
302
|
+
if (Repository?.isRemote) {
|
|
303
|
+
// Search tree on server
|
|
304
|
+
found = await Repository.searchNodes(value);
|
|
305
|
+
} else {
|
|
306
|
+
// Search local tree data
|
|
307
|
+
found = findTreeNodesByText(value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const isMultipleHits = found.length > 1;
|
|
311
|
+
if (!isMultipleHits) {
|
|
312
|
+
expandPath(found[0].path);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const searchFormData = [];
|
|
317
|
+
_.each(found, (item) => {
|
|
318
|
+
searchFormData.push([item.id, getNodeText(item)]);
|
|
319
|
+
});
|
|
320
|
+
setSearchFormData(searchFormData);
|
|
321
|
+
setSearchResults(found);
|
|
322
|
+
setIsSearchModalShown(true);
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// utilities
|
|
326
|
+
getNodeData = (itemId) => {
|
|
327
|
+
function findNodeById(node, id) {
|
|
328
|
+
if (node.item.id === id) {
|
|
329
|
+
return node;
|
|
330
|
+
}
|
|
331
|
+
if (!_.isEmpty(node.children)) {
|
|
332
|
+
return _.find(node.children, (node2) => {
|
|
333
|
+
return findNodeById(node2, id);
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
let found = null;
|
|
339
|
+
_.each(getTreeNodeData(), (node) => {
|
|
340
|
+
const foundNode = findNodeById(node, itemId);
|
|
341
|
+
if (foundNode) {
|
|
342
|
+
found = foundNode;
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return found;
|
|
275
347
|
},
|
|
276
348
|
buildTreeNodeDatum = (treeNode) => {
|
|
277
349
|
// Build the data-representation of one node and its children,
|
|
@@ -286,7 +358,7 @@ function TreeComponent(props) {
|
|
|
286
358
|
iconCollapsed: getNodeIcon(treeNode, false),
|
|
287
359
|
iconExpanded: getNodeIcon(treeNode, true),
|
|
288
360
|
iconLeaf: getNodeIcon(treeNode),
|
|
289
|
-
isExpanded: isRoot, // all non-root treeNodes are
|
|
361
|
+
isExpanded: isRoot, // all non-root treeNodes are collapsed by default
|
|
290
362
|
isVisible: isRoot ? areRootsVisible : true,
|
|
291
363
|
isLoading: false,
|
|
292
364
|
children,
|
|
@@ -301,131 +373,56 @@ function TreeComponent(props) {
|
|
|
301
373
|
});
|
|
302
374
|
return data;
|
|
303
375
|
},
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
376
|
+
buildAndSetTreeNodeData = async () => {
|
|
377
|
+
let nodes = [];
|
|
378
|
+
if (Repository) {
|
|
379
|
+
if (!Repository.areRootNodesLoaded) {
|
|
380
|
+
nodes = await Repository.loadRootNodes(1);
|
|
381
|
+
} else {
|
|
382
|
+
nodes = Repository.getRootNodes();
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
nodes = assembleDataTreeNodes();
|
|
313
386
|
}
|
|
314
387
|
|
|
315
|
-
|
|
316
|
-
|
|
388
|
+
setTreeNodeData(buildTreeNodeData(nodes));
|
|
389
|
+
},
|
|
390
|
+
datumContainsSelection = (datum) => {
|
|
391
|
+
if (_.isEmpty(selection)) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
const
|
|
395
|
+
selectionIds = _.map(selection, (item) => item.id),
|
|
396
|
+
datumIds = getDatumChildIds(datum),
|
|
397
|
+
intersection = selectionIds.filter(x => datumIds.includes(x));
|
|
317
398
|
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
if (isReorderMode) {
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
switch (e.detail) {
|
|
329
|
-
case 1: // single click
|
|
330
|
-
onNodeClick(item, e); // sets selection
|
|
331
|
-
break;
|
|
332
|
-
case 2: // double click
|
|
333
|
-
if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
|
|
334
|
-
onNodeClick(item, e); // so reselect it
|
|
335
|
-
}
|
|
336
|
-
if (onEdit) {
|
|
337
|
-
onEdit();
|
|
338
|
-
}
|
|
339
|
-
break;
|
|
340
|
-
case 3: // triple click
|
|
341
|
-
break;
|
|
342
|
-
default:
|
|
343
|
-
}
|
|
344
|
-
}}
|
|
345
|
-
onLongPress={(e) => {
|
|
346
|
-
if (e.preventDefault && e.cancelable) {
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
}
|
|
349
|
-
if (isReorderMode) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
399
|
+
return !_.isEmpty(intersection);
|
|
400
|
+
},
|
|
401
|
+
findTreeNodesByText = (text) => {
|
|
402
|
+
// Helper for onSearchTree
|
|
403
|
+
// Searches whole treeNodeData for any matching items
|
|
404
|
+
// Returns multiple nodes
|
|
352
405
|
|
|
353
|
-
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// context menu
|
|
358
|
-
const selection = [item];
|
|
359
|
-
setSelection(selection);
|
|
360
|
-
if (onContextMenu) {
|
|
361
|
-
onContextMenu(item, e, selection, setSelection);
|
|
362
|
-
}
|
|
363
|
-
}}
|
|
364
|
-
flexDirection="row"
|
|
365
|
-
ml={((areRootsVisible ? depth : depth -1) * 20) + 'px'}
|
|
366
|
-
>
|
|
367
|
-
{({
|
|
368
|
-
isHovered,
|
|
369
|
-
isFocused,
|
|
370
|
-
isPressed,
|
|
371
|
-
}) => {
|
|
372
|
-
let bg = nodeProps.bg || styles.TREE_NODE_BG,
|
|
373
|
-
mixWith;
|
|
374
|
-
if (isSelected) {
|
|
375
|
-
if (showHovers && isHovered) {
|
|
376
|
-
mixWith = styles.TREE_NODE_SELECTED_HOVER_BG;
|
|
377
|
-
} else {
|
|
378
|
-
mixWith = styles.TREE_NODE_SELECTED_BG;
|
|
379
|
-
}
|
|
380
|
-
} else if (showHovers && isHovered) {
|
|
381
|
-
mixWith = styles.TREE_NODE_HOVER_BG;
|
|
382
|
-
}
|
|
383
|
-
if (mixWith) {
|
|
384
|
-
const
|
|
385
|
-
mixWithObj = nbToRgb(mixWith),
|
|
386
|
-
ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
|
|
387
|
-
bg = colourMixer.blend(bg, ratio, mixWithObj.color);
|
|
388
|
-
}
|
|
389
|
-
let WhichTreeNode = TreeNode,
|
|
390
|
-
rowReorderProps = {};
|
|
391
|
-
if (canNodesReorder && isReorderMode) {
|
|
392
|
-
WhichTreeNode = ReorderableTreeNode;
|
|
393
|
-
rowReorderProps = {
|
|
394
|
-
mode: VERTICAL,
|
|
395
|
-
onDragStart: onNodeReorderDragStart,
|
|
396
|
-
onDrag: onNodeReorderDrag,
|
|
397
|
-
onDragStop: onNodeReorderDragStop,
|
|
398
|
-
proxyParent: treeRef.current?.getScrollableNode().children[0],
|
|
399
|
-
proxyPositionRelativeToParent: true,
|
|
400
|
-
getParentNode: (node) => node.parentElement.parentElement.parentElement,
|
|
401
|
-
getProxy: getReorderProxy,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return <WhichTreeNode
|
|
406
|
-
nodeProps={nodeProps}
|
|
407
|
-
bg={bg}
|
|
408
|
-
datum={datum}
|
|
409
|
-
onToggle={onToggle}
|
|
406
|
+
const regex = new RegExp(text, 'i'); // instead of matching based on full text match, search for a partial match
|
|
410
407
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
408
|
+
function searchChildren(children, found = []) {
|
|
409
|
+
_.each(children, (child) => {
|
|
410
|
+
if (child.text.match(regex)) {
|
|
411
|
+
found.push(child);
|
|
412
|
+
}
|
|
413
|
+
if (child.children) {
|
|
414
|
+
searchChildren(child.children, found);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
return found;
|
|
418
|
+
}
|
|
419
|
+
return searchChildren(treeNodeData);
|
|
416
420
|
},
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (datum.children.length && datum.isExpanded) {
|
|
424
|
-
const childTreeNodes = renderTreeNodes(datum.children); // recursion
|
|
425
|
-
nodes = nodes.concat(childTreeNodes);
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
return nodes;
|
|
421
|
+
getTreeNodeByNodeId = (node_id) => {
|
|
422
|
+
if (Repository) {
|
|
423
|
+
return Repository.getById(node_id);
|
|
424
|
+
}
|
|
425
|
+
return data[node_id]; // TODO: This is probably not right!
|
|
429
426
|
},
|
|
430
427
|
getDatumChildIds = (datum) => {
|
|
431
428
|
let ids = [];
|
|
@@ -439,30 +436,6 @@ function TreeComponent(props) {
|
|
|
439
436
|
});
|
|
440
437
|
return ids;
|
|
441
438
|
},
|
|
442
|
-
datumContainsSelection = (datum) => {
|
|
443
|
-
if (_.isEmpty(selection)) {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
const
|
|
447
|
-
selectionIds = _.map(selection, (item) => item.id),
|
|
448
|
-
datumIds = getDatumChildIds(datum),
|
|
449
|
-
intersection = selectionIds.filter(x => datumIds.includes(x));
|
|
450
|
-
|
|
451
|
-
return !_.isEmpty(intersection);
|
|
452
|
-
},
|
|
453
|
-
buildAndSetTreeNodeData = async () => {
|
|
454
|
-
let rootNodes;
|
|
455
|
-
if (Repository) {
|
|
456
|
-
if (!Repository.areRootNodesLoaded) {
|
|
457
|
-
rootNodes = await Repository.getRootNodes(1);
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
rootNodes = assembleDataTreeNodes();
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const treeNodeData = buildTreeNodeData(rootNodes);
|
|
464
|
-
setTreeNodeData(treeNodeData);
|
|
465
|
-
},
|
|
466
439
|
assembleDataTreeNodes = () => {
|
|
467
440
|
// Populates the TreeNodes with .parent and .children references
|
|
468
441
|
// NOTE: This is only for 'data', not for Repositories!
|
|
@@ -502,47 +475,27 @@ function TreeComponent(props) {
|
|
|
502
475
|
treeNode.depth = i;
|
|
503
476
|
treeNode.hash = treeNode[idIx];
|
|
504
477
|
|
|
505
|
-
if (treeNode.isRoot) {
|
|
506
|
-
treeNodes.push(treeNode);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
return treeNodes;
|
|
511
|
-
},
|
|
512
|
-
reloadTree = () => {
|
|
513
|
-
Repository.areRootNodesLoaded = false;
|
|
514
|
-
return buildAndSetTreeNodeData();
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
// Button handlers
|
|
518
|
-
onToggle = (datum) => {
|
|
519
|
-
if (datum.isLoading) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
datum.isExpanded = !datum.isExpanded;
|
|
524
|
-
|
|
525
|
-
if (datum.isExpanded && datum.item.repository?.isRemote && datum.item.hasChildren && !datum.item.areChildrenLoaded) {
|
|
526
|
-
loadChildren(datum, 1);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
478
|
+
if (treeNode.isRoot) {
|
|
479
|
+
treeNodes.push(treeNode);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
529
482
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
483
|
+
return treeNodes;
|
|
484
|
+
},
|
|
485
|
+
reloadTree = () => {
|
|
486
|
+
Repository.areRootNodesLoaded = false;
|
|
487
|
+
return buildAndSetTreeNodeData();
|
|
535
488
|
},
|
|
536
|
-
loadChildren = async (datum, depth) => {
|
|
489
|
+
loadChildren = async (datum, depth = 1) => {
|
|
537
490
|
// Show loading indicator (spinner underneath current node?)
|
|
538
491
|
datum.isLoading = true;
|
|
539
492
|
forceUpdate();
|
|
540
493
|
|
|
541
494
|
try {
|
|
542
495
|
|
|
543
|
-
const children = await datum.item.loadChildren(
|
|
544
|
-
|
|
545
|
-
datum.
|
|
496
|
+
const children = await datum.item.loadChildren(depth);
|
|
497
|
+
datum.children = buildTreeNodeData(children);
|
|
498
|
+
datum.isExpanded = true;
|
|
546
499
|
|
|
547
500
|
} catch (err) {
|
|
548
501
|
// TODO: how do I handle errors?
|
|
@@ -556,16 +509,6 @@ function TreeComponent(props) {
|
|
|
556
509
|
datum.isLoading = false;
|
|
557
510
|
forceUpdate();
|
|
558
511
|
},
|
|
559
|
-
onCollapseAll = (setNewTreeNodeData = true) => {
|
|
560
|
-
// Go through whole tree and collapse all nodes
|
|
561
|
-
const newTreeNodeData = _.clone(treeNodeData);
|
|
562
|
-
collapseNodes(newTreeNodeData);
|
|
563
|
-
|
|
564
|
-
if (setNewTreeNodeData) {
|
|
565
|
-
setTreeNodeData(newTreeNodeData);
|
|
566
|
-
}
|
|
567
|
-
return newTreeNodeData;
|
|
568
|
-
},
|
|
569
512
|
collapseNodes = (nodes) => {
|
|
570
513
|
_.each(nodes, (node) => {
|
|
571
514
|
node.isExpanded = false;
|
|
@@ -574,61 +517,9 @@ function TreeComponent(props) {
|
|
|
574
517
|
}
|
|
575
518
|
});
|
|
576
519
|
},
|
|
577
|
-
onSearchTree = async (value) => {
|
|
578
|
-
let found = [];
|
|
579
|
-
if (Repository?.isRemote) {
|
|
580
|
-
// Search tree on server
|
|
581
|
-
found = await Repository.searchNodes(value);
|
|
582
|
-
} else {
|
|
583
|
-
// Search local tree data
|
|
584
|
-
found = findTreeNodesByText(value);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const isMultipleHits = found.length > 1;
|
|
588
|
-
if (!isMultipleHits) {
|
|
589
|
-
expandPath(found[0].path);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const searchFormData = [];
|
|
594
|
-
_.each(found, (item) => {
|
|
595
|
-
searchFormData.push([item.id, getNodeText(item)]);
|
|
596
|
-
});
|
|
597
|
-
setSearchFormData(searchFormData);
|
|
598
|
-
setSearchResults(found);
|
|
599
|
-
setIsSearchModalShown(true);
|
|
600
|
-
},
|
|
601
|
-
findTreeNodesByText = (text) => {
|
|
602
|
-
// Helper for onSearchTree
|
|
603
|
-
// Searches whole treeNodeData for any matching items
|
|
604
|
-
// Returns multiple nodes
|
|
605
|
-
|
|
606
|
-
const regex = new RegExp(text, 'i'); // instead of matching based on full text match, search for a partial match
|
|
607
|
-
|
|
608
|
-
function searchChildren(children, found = []) {
|
|
609
|
-
_.each(children, (child) => {
|
|
610
|
-
if (child.text.match(regex)) {
|
|
611
|
-
found.push(child);
|
|
612
|
-
}
|
|
613
|
-
if (child.children) {
|
|
614
|
-
searchChildren(child.children, found);
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
return found;
|
|
618
|
-
}
|
|
619
|
-
return searchChildren(treeNodeData);
|
|
620
|
-
},
|
|
621
|
-
getTreeNodeByNodeId = (node_id) => {
|
|
622
|
-
if (Repository) {
|
|
623
|
-
return Repository.getById(node_id);
|
|
624
|
-
}
|
|
625
|
-
return data[node_id]; // TODO: This is probably not right!
|
|
626
|
-
},
|
|
627
520
|
expandPath = async (path) => {
|
|
628
|
-
// Helper for onSearchTree
|
|
629
|
-
|
|
630
521
|
// First, close thw whole tree.
|
|
631
|
-
let newTreeNodeData = _.clone(
|
|
522
|
+
let newTreeNodeData = _.clone(getTreeNodeData());
|
|
632
523
|
collapseNodes(newTreeNodeData);
|
|
633
524
|
|
|
634
525
|
// As it navigates down, it will expand the appropriate branches,
|
|
@@ -692,7 +583,185 @@ function TreeComponent(props) {
|
|
|
692
583
|
|
|
693
584
|
},
|
|
694
585
|
|
|
695
|
-
//
|
|
586
|
+
// render
|
|
587
|
+
getHeaderToolbarItems = () => {
|
|
588
|
+
const
|
|
589
|
+
buttons = [
|
|
590
|
+
{
|
|
591
|
+
key: 'searchBtn',
|
|
592
|
+
text: 'Search tree',
|
|
593
|
+
handler: onSearchTree,
|
|
594
|
+
icon: MagnifyingGlass,
|
|
595
|
+
isDisabled: !treeSearchValue.length,
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
key: 'collapseBtn',
|
|
599
|
+
text: 'Collapse whole tree',
|
|
600
|
+
handler: onCollapseAll,
|
|
601
|
+
icon: Collapse,
|
|
602
|
+
isDisabled: false,
|
|
603
|
+
},
|
|
604
|
+
];
|
|
605
|
+
if (canNodesReorder) {
|
|
606
|
+
buttons.push({
|
|
607
|
+
key: 'reorderBtn',
|
|
608
|
+
text: 'Enter reorder mode',
|
|
609
|
+
handler: () => setIsReorderMode(!isReorderMode),
|
|
610
|
+
icon: isReorderMode ? NoReorderRows : ReorderRows,
|
|
611
|
+
isDisabled: false,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const items = _.map(buttons, getIconButtonFromConfig);
|
|
615
|
+
|
|
616
|
+
items.unshift(<Input // Add text input to beginning of header items
|
|
617
|
+
key="searchNodes"
|
|
618
|
+
flex={1}
|
|
619
|
+
placeholder="Find tree node"
|
|
620
|
+
onChangeText={(val) => setTreeSearchValue(val)}
|
|
621
|
+
onKeyPress={(e, value) => {
|
|
622
|
+
if (e.key === 'Enter') {
|
|
623
|
+
onSearchTree(value);
|
|
624
|
+
}
|
|
625
|
+
}}
|
|
626
|
+
value={treeSearchValue}
|
|
627
|
+
autoSubmit={false}
|
|
628
|
+
/>);
|
|
629
|
+
|
|
630
|
+
return items;
|
|
631
|
+
},
|
|
632
|
+
getFooterToolbarItems = () => {
|
|
633
|
+
return _.map(additionalToolbarButtons, getIconButtonFromConfig);
|
|
634
|
+
},
|
|
635
|
+
renderTreeNode = (datum) => {
|
|
636
|
+
if (!datum.isVisible) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
const item = datum.item;
|
|
640
|
+
if (item.isDestroyed) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const depth = item.depth;
|
|
644
|
+
|
|
645
|
+
let nodeProps = getNodeProps ? getNodeProps(item) : {},
|
|
646
|
+
isSelected = isInSelection(item);
|
|
647
|
+
|
|
648
|
+
return <Pressable
|
|
649
|
+
// {...testProps(Repository ? Repository.schema.name + '-' + item.id : item.id)}
|
|
650
|
+
key={item.hash}
|
|
651
|
+
onPress={(e) => {
|
|
652
|
+
if (e.preventDefault && e.cancelable) {
|
|
653
|
+
e.preventDefault();
|
|
654
|
+
}
|
|
655
|
+
if (isReorderMode) {
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
switch (e.detail) {
|
|
659
|
+
case 1: // single click
|
|
660
|
+
onNodeClick(item, e); // sets selection
|
|
661
|
+
break;
|
|
662
|
+
case 2: // double click
|
|
663
|
+
if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
|
|
664
|
+
onNodeClick(item, e); // so reselect it
|
|
665
|
+
}
|
|
666
|
+
if (onEdit) {
|
|
667
|
+
onEdit();
|
|
668
|
+
}
|
|
669
|
+
break;
|
|
670
|
+
case 3: // triple click
|
|
671
|
+
break;
|
|
672
|
+
default:
|
|
673
|
+
}
|
|
674
|
+
}}
|
|
675
|
+
onLongPress={(e) => {
|
|
676
|
+
if (e.preventDefault && e.cancelable) {
|
|
677
|
+
e.preventDefault();
|
|
678
|
+
}
|
|
679
|
+
if (isReorderMode) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (!setSelection) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// context menu
|
|
688
|
+
const selection = [item];
|
|
689
|
+
setSelection(selection);
|
|
690
|
+
if (onContextMenu) {
|
|
691
|
+
onContextMenu(item, e, selection, setSelection);
|
|
692
|
+
}
|
|
693
|
+
}}
|
|
694
|
+
flexDirection="row"
|
|
695
|
+
ml={((areRootsVisible ? depth : depth -1) * 20) + 'px'}
|
|
696
|
+
>
|
|
697
|
+
{({
|
|
698
|
+
isHovered,
|
|
699
|
+
isFocused,
|
|
700
|
+
isPressed,
|
|
701
|
+
}) => {
|
|
702
|
+
let bg = nodeProps.bg || styles.TREE_NODE_BG,
|
|
703
|
+
mixWith;
|
|
704
|
+
if (isSelected) {
|
|
705
|
+
if (showHovers && isHovered) {
|
|
706
|
+
mixWith = styles.TREE_NODE_SELECTED_HOVER_BG;
|
|
707
|
+
} else {
|
|
708
|
+
mixWith = styles.TREE_NODE_SELECTED_BG;
|
|
709
|
+
}
|
|
710
|
+
} else if (showHovers && isHovered) {
|
|
711
|
+
mixWith = styles.TREE_NODE_HOVER_BG;
|
|
712
|
+
}
|
|
713
|
+
if (mixWith) {
|
|
714
|
+
const
|
|
715
|
+
mixWithObj = nbToRgb(mixWith),
|
|
716
|
+
ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
|
|
717
|
+
bg = colourMixer.blend(bg, ratio, mixWithObj.color);
|
|
718
|
+
}
|
|
719
|
+
let WhichTreeNode = TreeNode,
|
|
720
|
+
rowReorderProps = {};
|
|
721
|
+
if (canNodesReorder && isReorderMode) {
|
|
722
|
+
WhichTreeNode = ReorderableTreeNode;
|
|
723
|
+
rowReorderProps = {
|
|
724
|
+
mode: VERTICAL,
|
|
725
|
+
onDragStart: onNodeReorderDragStart,
|
|
726
|
+
onDrag: onNodeReorderDrag,
|
|
727
|
+
onDragStop: onNodeReorderDragStop,
|
|
728
|
+
proxyParent: treeRef.current?.getScrollableNode().children[0],
|
|
729
|
+
proxyPositionRelativeToParent: true,
|
|
730
|
+
getParentNode: (node) => node.parentElement.parentElement.parentElement,
|
|
731
|
+
getProxy: getReorderProxy,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return <WhichTreeNode
|
|
736
|
+
nodeProps={nodeProps}
|
|
737
|
+
bg={bg}
|
|
738
|
+
datum={datum}
|
|
739
|
+
onToggle={onToggle}
|
|
740
|
+
|
|
741
|
+
// fields={fields}
|
|
742
|
+
{...rowReorderProps}
|
|
743
|
+
/>;
|
|
744
|
+
}}
|
|
745
|
+
</Pressable>;
|
|
746
|
+
},
|
|
747
|
+
renderTreeNodes = (data) => {
|
|
748
|
+
let nodes = [];
|
|
749
|
+
_.each(data, (datum) => {
|
|
750
|
+
const node = renderTreeNode(datum);
|
|
751
|
+
if (!node) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
nodes.push(node);
|
|
755
|
+
|
|
756
|
+
if (datum.children.length && datum.isExpanded) {
|
|
757
|
+
const childTreeNodes = renderTreeNodes(datum.children); // recursion
|
|
758
|
+
nodes = nodes.concat(childTreeNodes);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
return nodes;
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
// drag/drop
|
|
696
765
|
getReorderProxy = (node) => {
|
|
697
766
|
const
|
|
698
767
|
row = node.parentElement.parentElement,
|
|
@@ -967,16 +1036,12 @@ function TreeComponent(props) {
|
|
|
967
1036
|
|
|
968
1037
|
Repository.on('beforeLoad', setTrue);
|
|
969
1038
|
Repository.on('load', setFalse);
|
|
970
|
-
Repository.ons(['changePage', 'changePageSize',], deselectAll);
|
|
971
|
-
Repository.ons(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
972
1039
|
Repository.on('changeFilters', reloadTree);
|
|
973
1040
|
Repository.on('changeSorters', reloadTree);
|
|
974
1041
|
|
|
975
1042
|
return () => {
|
|
976
1043
|
Repository.off('beforeLoad', setTrue);
|
|
977
1044
|
Repository.off('load', setFalse);
|
|
978
|
-
Repository.offs(['changePage', 'changePageSize',], deselectAll);
|
|
979
|
-
Repository.offs(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
980
1045
|
Repository.off('changeFilters', reloadTree);
|
|
981
1046
|
Repository.off('changeSorters', reloadTree);
|
|
982
1047
|
};
|
|
@@ -993,17 +1058,26 @@ function TreeComponent(props) {
|
|
|
993
1058
|
}
|
|
994
1059
|
Repository.filter(selectorId, id, false); // so it doesn't clear existing filters
|
|
995
1060
|
}
|
|
996
|
-
|
|
997
1061
|
}, [selectorId, selectorSelected]);
|
|
998
1062
|
|
|
1063
|
+
setWithEditListeners({ // Update withEdit's listeners on every render
|
|
1064
|
+
onBeforeAdd,
|
|
1065
|
+
onAfterAdd,
|
|
1066
|
+
onBeforeEditSave,
|
|
1067
|
+
onAfterEdit,
|
|
1068
|
+
onBeforeDeleteSave,
|
|
1069
|
+
onAfterDelete,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
999
1072
|
const
|
|
1000
|
-
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue,
|
|
1001
|
-
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode,
|
|
1073
|
+
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue, getTreeNodeData()]),
|
|
1074
|
+
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode, getTreeNodeData()]);
|
|
1002
1075
|
|
|
1003
1076
|
if (!isReady) {
|
|
1004
1077
|
return null;
|
|
1005
1078
|
}
|
|
1006
|
-
|
|
1079
|
+
|
|
1080
|
+
const treeNodes = renderTreeNodes(getTreeNodeData());
|
|
1007
1081
|
|
|
1008
1082
|
// headers & footers
|
|
1009
1083
|
let treeFooterComponent = null;
|
|
@@ -1029,7 +1103,7 @@ function TreeComponent(props) {
|
|
|
1029
1103
|
deselectAll();
|
|
1030
1104
|
}
|
|
1031
1105
|
}}>
|
|
1032
|
-
{!treeNodes?.length ? <NoRecordsFound text={noneFoundText} onRefresh={
|
|
1106
|
+
{!treeNodes?.length ? <NoRecordsFound text={noneFoundText} onRefresh={reloadTree} /> :
|
|
1033
1107
|
treeNodes}
|
|
1034
1108
|
</Column>
|
|
1035
1109
|
|