@onehat/ui 0.2.73 → 0.2.75
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/Grid/Grid.js +18 -53
- package/src/Components/Hoc/withAlert.js +42 -39
- package/src/Components/Hoc/withEditor.js +116 -16
- package/src/Components/Hoc/withPresetButtons.js +4 -0
- package/src/Components/Hoc/withSideEditor.js +2 -2
- package/src/Components/Hoc/withWindowedEditor.js +2 -2
- package/src/Components/Tree/Tree.js +394 -354
- package/src/Constants/Styles.js +3 -6
- package/src/Functions/getIconButtonFromConfig.js +37 -0
|
@@ -32,6 +32,7 @@ import withPresetButtons from '../Hoc/withPresetButtons.js';
|
|
|
32
32
|
import withMultiSelection from '../Hoc/withMultiSelection.js';
|
|
33
33
|
import withSelection from '../Hoc/withSelection.js';
|
|
34
34
|
import withWindowedEditor from '../Hoc/withWindowedEditor.js';
|
|
35
|
+
import getIconButtonFromConfig from '../../Functions/getIconButtonFromConfig.js';
|
|
35
36
|
import testProps from '../../Functions/testProps.js';
|
|
36
37
|
import nbToRgb from '../../Functions/nbToRgb.js';
|
|
37
38
|
import TreeNode, { ReorderableTreeNode } from './TreeNode.js';
|
|
@@ -51,19 +52,7 @@ import Toolbar from '../Toolbar/Toolbar.js';
|
|
|
51
52
|
import _ from 'lodash';
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
//////////////////////
|
|
56
|
-
|
|
57
|
-
// Need to take into account whether using Repository or data.
|
|
58
|
-
// If using data, everything exists at once. What format will data be in?
|
|
59
|
-
// How does this interface with Repository?
|
|
60
|
-
// Maybe if Repository is not AjaxRepository, everything needs to be present at once!
|
|
61
|
-
|
|
62
|
-
//////////////////////
|
|
63
|
-
//////////////////////
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export function TreeComponent(props) {
|
|
55
|
+
function TreeComponent(props) {
|
|
67
56
|
const {
|
|
68
57
|
areRootsVisible = true,
|
|
69
58
|
extraParams = {}, // Additional params to send with each request ( e.g. { order: 'Categories.name ASC' })
|
|
@@ -102,7 +91,7 @@ export function TreeComponent(props) {
|
|
|
102
91
|
additionalToolbarButtons = [],
|
|
103
92
|
reload = null, // Whenever this value changes after initial render, the tree will reload from scratch
|
|
104
93
|
parentIdIx,
|
|
105
|
-
|
|
94
|
+
|
|
106
95
|
// withEditor
|
|
107
96
|
onAdd,
|
|
108
97
|
onEdit,
|
|
@@ -111,6 +100,7 @@ export function TreeComponent(props) {
|
|
|
111
100
|
onDuplicate,
|
|
112
101
|
onReset,
|
|
113
102
|
onContextMenu,
|
|
103
|
+
setWithEditListeners,
|
|
114
104
|
|
|
115
105
|
// withData
|
|
116
106
|
Repository,
|
|
@@ -140,16 +130,27 @@ export function TreeComponent(props) {
|
|
|
140
130
|
styles = UiGlobals.styles,
|
|
141
131
|
forceUpdate = useForceUpdate(),
|
|
142
132
|
treeRef = useRef(),
|
|
133
|
+
treeNodeData = useRef(),
|
|
143
134
|
[isReady, setIsReady] = useState(false),
|
|
144
135
|
[isLoading, setIsLoading] = useState(false),
|
|
145
136
|
[isReorderMode, setIsReorderMode] = useState(false),
|
|
146
137
|
[isSearchModalShown, setIsSearchModalShown] = useState(false),
|
|
147
|
-
[treeNodeData, setTreeNodeData] = useState({}),
|
|
148
138
|
[searchResults, setSearchResults] = useState([]),
|
|
149
139
|
[searchFormData, setSearchFormData] = useState([]),
|
|
150
140
|
[dragNodeSlot, setDragNodeSlot] = useState(null),
|
|
151
141
|
[dragNodeIx, setDragNodeIx] = useState(),
|
|
152
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
|
|
153
154
|
onNodeClick = (item, e) => {
|
|
154
155
|
if (!setSelection) {
|
|
155
156
|
return;
|
|
@@ -212,98 +213,137 @@ export function TreeComponent(props) {
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
},
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
forceUpdate();
|
|
224
|
-
});
|
|
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);
|
|
225
224
|
}
|
|
226
225
|
},
|
|
227
|
-
|
|
226
|
+
onAfterAdd = async (entity) => {
|
|
227
|
+
// Expand the parent before showing the new node
|
|
228
228
|
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
icon: MagnifyingGlass,
|
|
235
|
-
isDisabled: !treeSearchValue.length,
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
key: 'collapseBtn',
|
|
239
|
-
text: 'Collapse whole tree',
|
|
240
|
-
handler: onCollapseAll,
|
|
241
|
-
icon: Collapse,
|
|
242
|
-
isDisabled: false,
|
|
243
|
-
},
|
|
244
|
-
];
|
|
245
|
-
if (canNodesReorder) {
|
|
246
|
-
buttons.push({
|
|
247
|
-
key: 'reorderBtn',
|
|
248
|
-
text: 'Enter reorder mode',
|
|
249
|
-
handler: () => setIsReorderMode(!isReorderMode),
|
|
250
|
-
icon: isReorderMode ? NoReorderRows : ReorderRows,
|
|
251
|
-
isDisabled: false,
|
|
252
|
-
});
|
|
229
|
+
parent = entity.parent,
|
|
230
|
+
parentDatum = getNodeData(parent.id);
|
|
231
|
+
|
|
232
|
+
if (!parentDatum.isExpanded) {
|
|
233
|
+
parentDatum.isExpanded = true;
|
|
253
234
|
}
|
|
254
|
-
const items = _.map(buttons, getIconFromConfig);
|
|
255
235
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
236
|
+
// Add the entity to the tree
|
|
237
|
+
const entityDatum = buildTreeNodeDatum(entity);
|
|
238
|
+
parentDatum.children.unshift(entityDatum);
|
|
239
|
+
forceUpdate();
|
|
240
|
+
},
|
|
241
|
+
onBeforeEditSave = (entities) => {
|
|
242
|
+
onBeforeSave(entities);
|
|
243
|
+
},
|
|
244
|
+
onAfterEdit = async (entities) => {
|
|
245
|
+
// Refresh the node's display
|
|
246
|
+
const
|
|
247
|
+
node = entities[0],
|
|
248
|
+
existingDatum = getNodeData(node.id), // TODO: Make this work for >1 entity
|
|
249
|
+
newDatum = buildTreeNodeDatum(node);
|
|
269
250
|
|
|
270
|
-
|
|
251
|
+
// copy the updated data to existingDatum
|
|
252
|
+
_.merge(existingDatum, newDatum);
|
|
253
|
+
existingDatum.isLoading = false;
|
|
254
|
+
forceUpdate();
|
|
271
255
|
},
|
|
272
|
-
|
|
273
|
-
|
|
256
|
+
onBeforeDeleteSave = (entities) => {
|
|
257
|
+
onBeforeSave(entities);
|
|
274
258
|
},
|
|
275
|
-
|
|
259
|
+
onBeforeSave = (entities) => {
|
|
276
260
|
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
datum.isExpanded = !datum.isExpanded;
|
|
278
|
+
|
|
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();
|
|
289
|
+
},
|
|
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;
|
|
307
347
|
},
|
|
308
348
|
buildTreeNodeDatum = (treeNode) => {
|
|
309
349
|
// Build the data-representation of one node and its children,
|
|
@@ -318,7 +358,7 @@ export function TreeComponent(props) {
|
|
|
318
358
|
iconCollapsed: getNodeIcon(treeNode, false),
|
|
319
359
|
iconExpanded: getNodeIcon(treeNode, true),
|
|
320
360
|
iconLeaf: getNodeIcon(treeNode),
|
|
321
|
-
isExpanded: isRoot, // all non-root treeNodes are
|
|
361
|
+
isExpanded: isRoot, // all non-root treeNodes are collapsed by default
|
|
322
362
|
isVisible: isRoot ? areRootsVisible : true,
|
|
323
363
|
isLoading: false,
|
|
324
364
|
children,
|
|
@@ -333,131 +373,56 @@ export function TreeComponent(props) {
|
|
|
333
373
|
});
|
|
334
374
|
return data;
|
|
335
375
|
},
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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();
|
|
345
386
|
}
|
|
346
387
|
|
|
347
|
-
|
|
348
|
-
|
|
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));
|
|
349
398
|
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
if (isReorderMode) {
|
|
358
|
-
return
|
|
359
|
-
}
|
|
360
|
-
switch (e.detail) {
|
|
361
|
-
case 1: // single click
|
|
362
|
-
onNodeClick(item, e); // sets selection
|
|
363
|
-
break;
|
|
364
|
-
case 2: // double click
|
|
365
|
-
if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
|
|
366
|
-
onNodeClick(item, e); // so reselect it
|
|
367
|
-
}
|
|
368
|
-
if (onEdit) {
|
|
369
|
-
onEdit();
|
|
370
|
-
}
|
|
371
|
-
break;
|
|
372
|
-
case 3: // triple click
|
|
373
|
-
break;
|
|
374
|
-
default:
|
|
375
|
-
}
|
|
376
|
-
}}
|
|
377
|
-
onLongPress={(e) => {
|
|
378
|
-
if (e.preventDefault && e.cancelable) {
|
|
379
|
-
e.preventDefault();
|
|
380
|
-
}
|
|
381
|
-
if (isReorderMode) {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
399
|
+
return !_.isEmpty(intersection);
|
|
400
|
+
},
|
|
401
|
+
findTreeNodesByText = (text) => {
|
|
402
|
+
// Helper for onSearchTree
|
|
403
|
+
// Searches whole treeNodeData for any matching items
|
|
404
|
+
// Returns multiple nodes
|
|
384
405
|
|
|
385
|
-
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// context menu
|
|
390
|
-
const selection = [item];
|
|
391
|
-
setSelection(selection);
|
|
392
|
-
if (onContextMenu) {
|
|
393
|
-
onContextMenu(item, e, selection, setSelection);
|
|
394
|
-
}
|
|
395
|
-
}}
|
|
396
|
-
flexDirection="row"
|
|
397
|
-
ml={((areRootsVisible ? depth : depth -1) * 20) + 'px'}
|
|
398
|
-
>
|
|
399
|
-
{({
|
|
400
|
-
isHovered,
|
|
401
|
-
isFocused,
|
|
402
|
-
isPressed,
|
|
403
|
-
}) => {
|
|
404
|
-
let bg = nodeProps.bg || styles.TREE_NODE_BG,
|
|
405
|
-
mixWith;
|
|
406
|
-
if (isSelected) {
|
|
407
|
-
if (showHovers && isHovered) {
|
|
408
|
-
mixWith = styles.TREE_NODE_SELECTED_HOVER_BG;
|
|
409
|
-
} else {
|
|
410
|
-
mixWith = styles.TREE_NODE_SELECTED_BG;
|
|
411
|
-
}
|
|
412
|
-
} else if (showHovers && isHovered) {
|
|
413
|
-
mixWith = styles.TREE_NODE_HOVER_BG;
|
|
414
|
-
}
|
|
415
|
-
if (mixWith) {
|
|
416
|
-
const
|
|
417
|
-
mixWithObj = nbToRgb(mixWith),
|
|
418
|
-
ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
|
|
419
|
-
bg = colourMixer.blend(bg, ratio, mixWithObj.color);
|
|
420
|
-
}
|
|
421
|
-
let WhichTreeNode = TreeNode,
|
|
422
|
-
rowReorderProps = {};
|
|
423
|
-
if (canNodesReorder && isReorderMode) {
|
|
424
|
-
WhichTreeNode = ReorderableTreeNode;
|
|
425
|
-
rowReorderProps = {
|
|
426
|
-
mode: VERTICAL,
|
|
427
|
-
onDragStart: onNodeReorderDragStart,
|
|
428
|
-
onDrag: onNodeReorderDrag,
|
|
429
|
-
onDragStop: onNodeReorderDragStop,
|
|
430
|
-
proxyParent: treeRef.current?.getScrollableNode().children[0],
|
|
431
|
-
proxyPositionRelativeToParent: true,
|
|
432
|
-
getParentNode: (node) => node.parentElement.parentElement.parentElement,
|
|
433
|
-
getProxy: getReorderProxy,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return <WhichTreeNode
|
|
438
|
-
nodeProps={nodeProps}
|
|
439
|
-
bg={bg}
|
|
440
|
-
datum={datum}
|
|
441
|
-
onToggle={onToggle}
|
|
406
|
+
const regex = new RegExp(text, 'i'); // instead of matching based on full text match, search for a partial match
|
|
442
407
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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);
|
|
448
420
|
},
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (datum.children.length && datum.isExpanded) {
|
|
456
|
-
const childTreeNodes = renderTreeNodes(datum.children); // recursion
|
|
457
|
-
nodes = nodes.concat(childTreeNodes);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
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!
|
|
461
426
|
},
|
|
462
427
|
getDatumChildIds = (datum) => {
|
|
463
428
|
let ids = [];
|
|
@@ -471,30 +436,6 @@ export function TreeComponent(props) {
|
|
|
471
436
|
});
|
|
472
437
|
return ids;
|
|
473
438
|
},
|
|
474
|
-
datumContainsSelection = (datum) => {
|
|
475
|
-
if (_.isEmpty(selection)) {
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
|
-
const
|
|
479
|
-
selectionIds = _.map(selection, (item) => item.id),
|
|
480
|
-
datumIds = getDatumChildIds(datum),
|
|
481
|
-
intersection = selectionIds.filter(x => datumIds.includes(x));
|
|
482
|
-
|
|
483
|
-
return !_.isEmpty(intersection);
|
|
484
|
-
},
|
|
485
|
-
buildAndSetTreeNodeData = async () => {
|
|
486
|
-
let rootNodes;
|
|
487
|
-
if (Repository) {
|
|
488
|
-
if (!Repository.areRootNodesLoaded) {
|
|
489
|
-
rootNodes = await Repository.getRootNodes(1);
|
|
490
|
-
}
|
|
491
|
-
} else {
|
|
492
|
-
rootNodes = assembleDataTreeNodes();
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const treeNodeData = buildTreeNodeData(rootNodes);
|
|
496
|
-
setTreeNodeData(treeNodeData);
|
|
497
|
-
},
|
|
498
439
|
assembleDataTreeNodes = () => {
|
|
499
440
|
// Populates the TreeNodes with .parent and .children references
|
|
500
441
|
// NOTE: This is only for 'data', not for Repositories!
|
|
@@ -544,37 +485,17 @@ export function TreeComponent(props) {
|
|
|
544
485
|
reloadTree = () => {
|
|
545
486
|
Repository.areRootNodesLoaded = false;
|
|
546
487
|
return buildAndSetTreeNodeData();
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
// Button handlers
|
|
550
|
-
onToggle = (datum) => {
|
|
551
|
-
if (datum.isLoading) {
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
datum.isExpanded = !datum.isExpanded;
|
|
556
|
-
|
|
557
|
-
if (datum.isExpanded && datum.item.repository?.isRemote && datum.item.hasChildren && !datum.item.areChildrenLoaded) {
|
|
558
|
-
loadChildren(datum, 1);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (!datum.isExpanded && datumContainsSelection(datum)) {
|
|
563
|
-
deselectAll();
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
forceUpdate();
|
|
567
488
|
},
|
|
568
|
-
loadChildren = async (datum, depth) => {
|
|
489
|
+
loadChildren = async (datum, depth = 1) => {
|
|
569
490
|
// Show loading indicator (spinner underneath current node?)
|
|
570
491
|
datum.isLoading = true;
|
|
571
492
|
forceUpdate();
|
|
572
493
|
|
|
573
494
|
try {
|
|
574
495
|
|
|
575
|
-
const children = await datum.item.loadChildren(
|
|
576
|
-
|
|
577
|
-
datum.
|
|
496
|
+
const children = await datum.item.loadChildren(depth);
|
|
497
|
+
datum.children = buildTreeNodeData(children);
|
|
498
|
+
datum.isExpanded = true;
|
|
578
499
|
|
|
579
500
|
} catch (err) {
|
|
580
501
|
// TODO: how do I handle errors?
|
|
@@ -588,16 +509,6 @@ export function TreeComponent(props) {
|
|
|
588
509
|
datum.isLoading = false;
|
|
589
510
|
forceUpdate();
|
|
590
511
|
},
|
|
591
|
-
onCollapseAll = (setNewTreeNodeData = true) => {
|
|
592
|
-
// Go through whole tree and collapse all nodes
|
|
593
|
-
const newTreeNodeData = _.clone(treeNodeData);
|
|
594
|
-
collapseNodes(newTreeNodeData);
|
|
595
|
-
|
|
596
|
-
if (setNewTreeNodeData) {
|
|
597
|
-
setTreeNodeData(newTreeNodeData);
|
|
598
|
-
}
|
|
599
|
-
return newTreeNodeData;
|
|
600
|
-
},
|
|
601
512
|
collapseNodes = (nodes) => {
|
|
602
513
|
_.each(nodes, (node) => {
|
|
603
514
|
node.isExpanded = false;
|
|
@@ -606,61 +517,9 @@ export function TreeComponent(props) {
|
|
|
606
517
|
}
|
|
607
518
|
});
|
|
608
519
|
},
|
|
609
|
-
onSearchTree = async (value) => {
|
|
610
|
-
let found = [];
|
|
611
|
-
if (Repository?.isRemote) {
|
|
612
|
-
// Search tree on server
|
|
613
|
-
found = await Repository.searchNodes(value);
|
|
614
|
-
} else {
|
|
615
|
-
// Search local tree data
|
|
616
|
-
found = findTreeNodesByText(value);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const isMultipleHits = found.length > 1;
|
|
620
|
-
if (!isMultipleHits) {
|
|
621
|
-
expandPath(found[0].path);
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const searchFormData = [];
|
|
626
|
-
_.each(found, (item) => {
|
|
627
|
-
searchFormData.push([item.id, getNodeText(item)]);
|
|
628
|
-
});
|
|
629
|
-
setSearchFormData(searchFormData);
|
|
630
|
-
setSearchResults(found);
|
|
631
|
-
setIsSearchModalShown(true);
|
|
632
|
-
},
|
|
633
|
-
findTreeNodesByText = (text) => {
|
|
634
|
-
// Helper for onSearchTree
|
|
635
|
-
// Searches whole treeNodeData for any matching items
|
|
636
|
-
// Returns multiple nodes
|
|
637
|
-
|
|
638
|
-
const regex = new RegExp(text, 'i'); // instead of matching based on full text match, search for a partial match
|
|
639
|
-
|
|
640
|
-
function searchChildren(children, found = []) {
|
|
641
|
-
_.each(children, (child) => {
|
|
642
|
-
if (child.text.match(regex)) {
|
|
643
|
-
found.push(child);
|
|
644
|
-
}
|
|
645
|
-
if (child.children) {
|
|
646
|
-
searchChildren(child.children, found);
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
return found;
|
|
650
|
-
}
|
|
651
|
-
return searchChildren(treeNodeData);
|
|
652
|
-
},
|
|
653
|
-
getTreeNodeByNodeId = (node_id) => {
|
|
654
|
-
if (Repository) {
|
|
655
|
-
return Repository.getById(node_id);
|
|
656
|
-
}
|
|
657
|
-
return data[node_id]; // TODO: This is probably not right!
|
|
658
|
-
},
|
|
659
520
|
expandPath = async (path) => {
|
|
660
|
-
// Helper for onSearchTree
|
|
661
|
-
|
|
662
521
|
// First, close thw whole tree.
|
|
663
|
-
let newTreeNodeData = _.clone(
|
|
522
|
+
let newTreeNodeData = _.clone(getTreeNodeData());
|
|
664
523
|
collapseNodes(newTreeNodeData);
|
|
665
524
|
|
|
666
525
|
// As it navigates down, it will expand the appropriate branches,
|
|
@@ -724,7 +583,185 @@ export function TreeComponent(props) {
|
|
|
724
583
|
|
|
725
584
|
},
|
|
726
585
|
|
|
727
|
-
//
|
|
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
|
|
728
765
|
getReorderProxy = (node) => {
|
|
729
766
|
const
|
|
730
767
|
row = node.parentElement.parentElement,
|
|
@@ -999,16 +1036,12 @@ export function TreeComponent(props) {
|
|
|
999
1036
|
|
|
1000
1037
|
Repository.on('beforeLoad', setTrue);
|
|
1001
1038
|
Repository.on('load', setFalse);
|
|
1002
|
-
Repository.ons(['changePage', 'changePageSize',], deselectAll);
|
|
1003
|
-
Repository.ons(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
1004
1039
|
Repository.on('changeFilters', reloadTree);
|
|
1005
1040
|
Repository.on('changeSorters', reloadTree);
|
|
1006
1041
|
|
|
1007
1042
|
return () => {
|
|
1008
1043
|
Repository.off('beforeLoad', setTrue);
|
|
1009
1044
|
Repository.off('load', setFalse);
|
|
1010
|
-
Repository.offs(['changePage', 'changePageSize',], deselectAll);
|
|
1011
|
-
Repository.offs(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
1012
1045
|
Repository.off('changeFilters', reloadTree);
|
|
1013
1046
|
Repository.off('changeSorters', reloadTree);
|
|
1014
1047
|
};
|
|
@@ -1025,17 +1058,26 @@ export function TreeComponent(props) {
|
|
|
1025
1058
|
}
|
|
1026
1059
|
Repository.filter(selectorId, id, false); // so it doesn't clear existing filters
|
|
1027
1060
|
}
|
|
1028
|
-
|
|
1029
1061
|
}, [selectorId, selectorSelected]);
|
|
1030
1062
|
|
|
1063
|
+
setWithEditListeners({ // Update withEdit's listeners on every render
|
|
1064
|
+
onBeforeAdd,
|
|
1065
|
+
onAfterAdd,
|
|
1066
|
+
onBeforeEditSave,
|
|
1067
|
+
onAfterEdit,
|
|
1068
|
+
onBeforeDeleteSave,
|
|
1069
|
+
onAfterDelete,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1031
1072
|
const
|
|
1032
|
-
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue,
|
|
1033
|
-
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode,
|
|
1073
|
+
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue, getTreeNodeData()]),
|
|
1074
|
+
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode, getTreeNodeData()]);
|
|
1034
1075
|
|
|
1035
1076
|
if (!isReady) {
|
|
1036
1077
|
return null;
|
|
1037
1078
|
}
|
|
1038
|
-
|
|
1079
|
+
|
|
1080
|
+
const treeNodes = renderTreeNodes(getTreeNodeData());
|
|
1039
1081
|
|
|
1040
1082
|
// headers & footers
|
|
1041
1083
|
let treeFooterComponent = null;
|
|
@@ -1061,7 +1103,7 @@ export function TreeComponent(props) {
|
|
|
1061
1103
|
deselectAll();
|
|
1062
1104
|
}
|
|
1063
1105
|
}}>
|
|
1064
|
-
{!treeNodes?.length ? <NoRecordsFound text={noneFoundText} onRefresh={
|
|
1106
|
+
{!treeNodes?.length ? <NoRecordsFound text={noneFoundText} onRefresh={reloadTree} /> :
|
|
1065
1107
|
treeNodes}
|
|
1066
1108
|
</Column>
|
|
1067
1109
|
|
|
@@ -1114,25 +1156,21 @@ export function TreeComponent(props) {
|
|
|
1114
1156
|
|
|
1115
1157
|
}
|
|
1116
1158
|
|
|
1117
|
-
const Tree = withAlert(
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
withFilters(
|
|
1124
|
-
// withPresetButtons(
|
|
1159
|
+
export const Tree = withAlert(
|
|
1160
|
+
withEvents(
|
|
1161
|
+
withData(
|
|
1162
|
+
// withMultiSelection(
|
|
1163
|
+
withSelection(
|
|
1164
|
+
withFilters(
|
|
1125
1165
|
withContextMenu(
|
|
1126
1166
|
TreeComponent
|
|
1127
1167
|
)
|
|
1128
|
-
|
|
1168
|
+
)
|
|
1129
1169
|
)
|
|
1130
1170
|
// )
|
|
1131
1171
|
)
|
|
1132
|
-
|
|
1133
|
-
)
|
|
1134
|
-
)
|
|
1135
|
-
);
|
|
1172
|
+
)
|
|
1173
|
+
);
|
|
1136
1174
|
|
|
1137
1175
|
export const SideTreeEditor = withAlert(
|
|
1138
1176
|
withEvents(
|
|
@@ -1146,7 +1184,8 @@ export const SideTreeEditor = withAlert(
|
|
|
1146
1184
|
TreeComponent
|
|
1147
1185
|
)
|
|
1148
1186
|
)
|
|
1149
|
-
)
|
|
1187
|
+
),
|
|
1188
|
+
true // isTree
|
|
1150
1189
|
)
|
|
1151
1190
|
)
|
|
1152
1191
|
// )
|
|
@@ -1166,7 +1205,8 @@ export const WindowedTreeEditor = withAlert(
|
|
|
1166
1205
|
TreeComponent
|
|
1167
1206
|
)
|
|
1168
1207
|
)
|
|
1169
|
-
)
|
|
1208
|
+
),
|
|
1209
|
+
true // isTree
|
|
1170
1210
|
)
|
|
1171
1211
|
)
|
|
1172
1212
|
// )
|