@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.
@@ -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
- onRefresh = () => {
216
- if (!Repository) {
217
- return;
218
- }
219
- const promise = Repository.reload();
220
- if (promise) { // Some repository types don't use promises
221
- promise.then(() => {
222
- setIsLoading(false);
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
- getHeaderToolbarItems = () => {
226
+ onAfterAdd = async (entity) => {
227
+ // Expand the parent before showing the new node
228
228
  const
229
- buttons = [
230
- {
231
- key: 'searchBtn',
232
- text: 'Search tree',
233
- handler: onSearchTree,
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
- items.unshift(<Input // Add text input to beginning of header items
257
- key="searchNodes"
258
- flex={1}
259
- placeholder="Find tree node"
260
- onChangeText={(val) => setTreeSearchValue(val)}
261
- onKeyPress={(e, value) => {
262
- if (e.key === 'Enter') {
263
- onSearchTree(value);
264
- }
265
- }}
266
- value={treeSearchValue}
267
- autoSubmit={false}
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
- return items;
251
+ // copy the updated data to existingDatum
252
+ _.merge(existingDatum, newDatum);
253
+ existingDatum.isLoading = false;
254
+ forceUpdate();
271
255
  },
272
- getFooterToolbarItems = () => {
273
- return _.map(additionalToolbarButtons, getIconFromConfig);
256
+ onBeforeDeleteSave = (entities) => {
257
+ onBeforeSave(entities);
274
258
  },
275
- getIconFromConfig = (config, ix) => {
259
+ onBeforeSave = (entities) => {
276
260
  const
277
- iconButtonProps = {
278
- _hover: {
279
- bg: 'trueGray.400',
280
- },
281
- mx: 1,
282
- px: 3,
283
- },
284
- _icon = {
285
- alignSelf: 'center',
286
- size: styles.TREE_TOOLBAR_ITEMS_ICON_SIZE,
287
- h: 20,
288
- w: 20,
289
- color: isDisabled ? styles.TREE_TOOLBAR_ITEMS_DISABLED_COLOR : styles.TREE_TOOLBAR_ITEMS_COLOR,
290
- };
291
- let {
292
- key,
293
- text,
294
- handler,
295
- icon = null,
296
- isDisabled = false,
297
- } = config;
298
- return <IconButton
299
- key={key || ix}
300
- onPress={handler}
301
- icon={icon}
302
- _icon={_icon}
303
- isDisabled={isDisabled}
304
- tooltip={text}
305
- {...iconButtonProps}
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 not expanded by default
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
- renderTreeNode = (datum) => {
337
- const
338
- item = datum.item,
339
- depth = item.depth;
340
- if (item.isDestroyed) {
341
- return null;
342
- }
343
- if (!datum.isVisible) {
344
- return null;
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
- let nodeProps = getNodeProps ? getNodeProps(item) : {},
348
- isSelected = isInSelection(item);
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 <Pressable
351
- // {...testProps(Repository ? Repository.schema.name + '-' + item.id : item.id)}
352
- key={item.hash}
353
- onPress={(e) => {
354
- if (e.preventDefault && e.cancelable) {
355
- e.preventDefault();
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
- if (!setSelection) {
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
- // fields={fields}
444
- {...rowReorderProps}
445
- />;
446
- }}
447
- </Pressable>;
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
- renderTreeNodes = (data) => {
450
- let nodes = [];
451
- _.each(data, (datum) => {
452
- const node = renderTreeNode(datum);
453
- nodes.push(node);
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(1);
576
- const tnd = buildTreeNodeData(children);
577
- datum.children = tnd;
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(treeNodeData);
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
- // Drag/Drop
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, treeNodeData]),
1033
- footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode, treeNodeData]);
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
- const treeNodes = renderTreeNodes(treeNodeData);
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={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
- withEvents(
1119
- withData(
1120
- // withMultiSelection(
1121
- withSelection(
1122
- // withSideEditor(
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
  // )