@onehat/ui 0.2.76 → 0.2.77

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.2.76",
3
+ "version": "0.2.77",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -223,7 +223,6 @@ function GridComponent(props) {
223
223
  if (canRowsReorder) {
224
224
  items.unshift(<IconButton
225
225
  key="reorderBtn"
226
- {...iconButtonProps}
227
226
  onPress={() => setIsReorderMode(!isReorderMode)}
228
227
  icon={<Icon as={isReorderMode ? NoReorderRows : ReorderRows} color={styles.GRID_TOOLBAR_ITEMS_COLOR} />}
229
228
  />);
@@ -388,7 +387,7 @@ function GridComponent(props) {
388
387
  row = node.parentElement.parentElement,
389
388
  parent = row.parentElement,
390
389
  parentRect = parent.getBoundingClientRect(),
391
- rows = _.filter(row.parentElement.children, (childNode) => {
390
+ rows = _.filter(parent.children, (childNode) => {
392
391
  return childNode.getBoundingClientRect().height !== 0; // Skip zero-height children
393
392
  }),
394
393
  currentY = proxyRect.top - parentRect.top, // top position of pointer, relative to page
@@ -473,7 +472,7 @@ function GridComponent(props) {
473
472
  row = node.parentElement.parentElement,
474
473
  parent = row.parentElement,
475
474
  parentRect = parent.getBoundingClientRect(),
476
- rows = _.filter(row.parentElement.children, (childNode) => {
475
+ rows = _.filter(parent.children, (childNode) => {
477
476
  return childNode.getBoundingClientRect().height !== 0; // Skip zero-height children
478
477
  }),
479
478
  currentY = proxyRect.top - parentRect.top, // top position of pointer, relative to page
@@ -537,7 +536,7 @@ function GridComponent(props) {
537
536
  const
538
537
  rowContainerRect = rows[newIx].getBoundingClientRect(),
539
538
  top = (useBottom ? rowContainerRect.bottom : rowContainerRect.top) - parentRect.top - parseInt(parent.style.borderWidth); // get relative Y position
540
- let marker = dragRowSlot && dragRowSlot.marker;
539
+ let marker = dragRowSlot?.marker;
541
540
  if (marker) {
542
541
  marker.style.top = top -4 + 'px'; // -4 so it's always visible
543
542
  }
@@ -782,9 +781,9 @@ function GridComponent(props) {
782
781
  nestedScrollEnabled={true}
783
782
  contentContainerStyle={{
784
783
  overflow: 'auto',
785
- borderWidth: isReorderMode ? 4 : 0,
786
- borderColor: isReorderMode ? '#23d9ea' : null,
787
- borderStyle: 'dashed',
784
+ borderWidth: isReorderMode ? styles.REORDER_BORDER_WIDTH : 0,
785
+ borderColor: isReorderMode ? styles.REORDER_BORDER_COLOR : null,
786
+ borderStyle: styles.REORDER_BORDER_STYLE,
788
787
  flex: 1,
789
788
  }}
790
789
  refreshing={isLoading}
@@ -29,11 +29,13 @@ export default function withDraggable(WrappedComponent) {
29
29
  onDrag,
30
30
  onDragStop,
31
31
  onChangeIsDragging,
32
+ getDraggableNodeFromNode = (node) => node,
32
33
  getParentNode = (node) => node.parentElement.parentElement,
33
34
  getProxy,
34
35
  proxyParent,
35
36
  proxyPositionRelativeToParent = false,
36
37
  handle,
38
+ draggableProps = {},
37
39
  ...propsToPass
38
40
  } = props,
39
41
  {
@@ -57,7 +59,7 @@ export default function withDraggable(WrappedComponent) {
57
59
  }
58
60
 
59
61
  const
60
- node = info.node,
62
+ node = getDraggableNodeFromNode(info.node),
61
63
  parentContainer = getParentNode && getParentNode(node);
62
64
 
63
65
  setNode(node);
@@ -226,8 +228,9 @@ export default function withDraggable(WrappedComponent) {
226
228
  onStop={handleStop}
227
229
  position={{ x: 0, y: 0, /* reset to dropped position */ }}
228
230
  // bounds={bounds}
231
+ {...draggableProps}
229
232
  >
230
- <div className="nsResize">
233
+ <div className="nsResize" style={{ width: '100%', }}>
231
234
  <WrappedComponent {...propsToPass} />
232
235
  </div>
233
236
  </Draggable>;
@@ -239,6 +242,7 @@ export default function withDraggable(WrappedComponent) {
239
242
  onStop={handleStop}
240
243
  position={{ x: 0, y: 0, /* reset to dropped position */ }}
241
244
  // bounds={bounds}
245
+ {...draggableProps}
242
246
  >
243
247
  <div className="ewResize" style={{ height: '100%', }}>
244
248
  <WrappedComponent {...propsToPass} />
@@ -252,8 +256,9 @@ export default function withDraggable(WrappedComponent) {
252
256
  onStart={handleStart}
253
257
  onDrag={handleDrag}
254
258
  onStop={handleStop}
255
- // position={{ x: 0, y: 0, /* reset to dropped position */ }}
259
+ position={{ x: 0, y: 0, /* reset to dropped position */ }}
256
260
  handle={handle}
261
+ {...draggableProps}
257
262
  >
258
263
  <WrappedComponent {...propsToPass} />
259
264
  </Draggable>;
@@ -165,7 +165,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
165
165
  await getListeners().onBeforeDeleteSave(selection);
166
166
  }
167
167
 
168
- await Repository.delete(selection);
168
+ await Repository.delete(selection, moveSubtreeUp);
169
169
  if (!Repository.isAutoSave) {
170
170
  await Repository.save();
171
171
  }
@@ -16,8 +16,9 @@ import {
16
16
  VERTICAL,
17
17
  } from '../../Constants/Directions.js';
18
18
  import {
19
- DROP_POSITION_BEFORE,
20
- DROP_POSITION_AFTER,
19
+ COLLAPSED,
20
+ EXPANDED,
21
+ LEAF,
21
22
  } from '../../Constants/Tree.js';
22
23
  import * as colourMixer from '@k-renwick/colour-mixer'
23
24
  import UiGlobals from '../../UiGlobals.js';
@@ -33,12 +34,12 @@ import withMultiSelection from '../Hoc/withMultiSelection.js';
33
34
  import withSelection from '../Hoc/withSelection.js';
34
35
  import withWindowedEditor from '../Hoc/withWindowedEditor.js';
35
36
  import getIconButtonFromConfig from '../../Functions/getIconButtonFromConfig.js';
37
+ import inArray from '../../Functions/inArray.js';
36
38
  import testProps from '../../Functions/testProps.js';
37
39
  import nbToRgb from '../../Functions/nbToRgb.js';
38
- import TreeNode, { ReorderableTreeNode } from './TreeNode.js';
40
+ import TreeNode, { DraggableTreeNode } from './TreeNode.js';
39
41
  import FormPanel from '../Panel/FormPanel.js';
40
42
  import Input from '../Form/Field/Input.js';
41
- import IconButton from '../Buttons/IconButton.js';
42
43
  import Dot from '../Icons/Dot.js';
43
44
  import Collapse from '../Icons/Collapse.js';
44
45
  import FolderClosed from '../Icons/FolderClosed.js';
@@ -51,6 +52,7 @@ import NoRecordsFound from '../Grid/NoRecordsFound.js';
51
52
  import Toolbar from '../Toolbar/Toolbar.js';
52
53
  import _ from 'lodash';
53
54
 
55
+ const DEPTH_INDENT_PX = 20;
54
56
 
55
57
  function TreeComponent(props) {
56
58
  const {
@@ -62,17 +64,19 @@ function TreeComponent(props) {
62
64
  }
63
65
  return item[displayIx];
64
66
  },
65
- getNodeIcon = (item, isExpanded) => { // decides what icon to show for this node
67
+ getNodeIcon = (which, item) => { // decides what icon to show for this node
66
68
  // TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
67
69
  let icon;
68
- if (item.hasChildren) {
69
- if (isExpanded) {
70
- icon = FolderOpen;
71
- } else {
70
+ switch(which) {
71
+ case COLLAPSED:
72
72
  icon = FolderClosed;
73
- }
74
- } else {
75
- icon = Dot;
73
+ break;
74
+ case EXPANDED:
75
+ icon = FolderOpen;
76
+ break;
77
+ case LEAF:
78
+ icon = Dot;
79
+ break;
76
80
  }
77
81
  return icon;
78
82
  },
@@ -133,12 +137,14 @@ function TreeComponent(props) {
133
137
  treeNodeData = useRef(),
134
138
  [isReady, setIsReady] = useState(false),
135
139
  [isLoading, setIsLoading] = useState(false),
136
- [isReorderMode, setIsReorderMode] = useState(false),
137
140
  [isSearchModalShown, setIsSearchModalShown] = useState(false),
141
+ [rowToDatumMap, setRowToDatumMap] = useState({}),
138
142
  [searchResults, setSearchResults] = useState([]),
139
143
  [searchFormData, setSearchFormData] = useState([]),
140
- [dragNodeSlot, setDragNodeSlot] = useState(null),
141
- [dragNodeIx, setDragNodeIx] = useState(),
144
+ [highlitedDatum, setHighlitedDatum] = useState(null),
145
+ [isDragMode, setIsDragMode] = useState(false),
146
+ [dragNodeId, setDragNodeId] = useState(null),
147
+ [dropRowIx, setDropRowIx] = useState(null),
142
148
  [treeSearchValue, setTreeSearchValue] = useState(''),
143
149
 
144
150
  // state getters & setters
@@ -237,6 +243,8 @@ function TreeComponent(props) {
237
243
  const entityDatum = buildTreeNodeDatum(entity);
238
244
  parentDatum.children.unshift(entityDatum);
239
245
  forceUpdate();
246
+
247
+ buildRowToDatumMap();
240
248
  },
241
249
  onBeforeEditSave = (entities) => {
242
250
  onBeforeSave(entities);
@@ -249,8 +257,18 @@ function TreeComponent(props) {
249
257
  newDatum = buildTreeNodeDatum(node);
250
258
 
251
259
  // copy the updated data to existingDatum
252
- _.merge(existingDatum, newDatum);
260
+ _.assign(existingDatum, newDatum);
253
261
  existingDatum.isLoading = false;
262
+
263
+
264
+ if (node.parent?.id) {
265
+ const
266
+ existingParentDatum = getNodeData(node.parent.id),
267
+ newParentDatum = buildTreeNodeDatum(node.parent);
268
+ _.assign(existingParentDatum, newParentDatum);
269
+ existingParentDatum.isExpanded = true;
270
+ }
271
+
254
272
  forceUpdate();
255
273
  },
256
274
  onBeforeDeleteSave = (entities) => {
@@ -265,9 +283,12 @@ function TreeComponent(props) {
265
283
  forceUpdate();
266
284
  },
267
285
  onAfterDelete = async (entities) => {
268
- // TODO: Refresh the parent node
269
-
270
- debugger;
286
+ const parent = entities[0].parent;
287
+ if (parent) {
288
+ await reloadNode(parent); // includes buildRowToDatumMap
289
+ } else {
290
+ buildRowToDatumMap();
291
+ }
271
292
  },
272
293
  onToggle = (datum) => {
273
294
  if (datum.isLoading) {
@@ -286,6 +307,8 @@ function TreeComponent(props) {
286
307
  }
287
308
 
288
309
  forceUpdate();
310
+
311
+ buildRowToDatumMap();
289
312
  },
290
313
  onCollapseAll = (setNewTreeNodeData = true) => {
291
314
  // Go through whole tree and collapse all nodes
@@ -295,6 +318,7 @@ function TreeComponent(props) {
295
318
  if (setNewTreeNodeData) {
296
319
  setTreeNodeData(newTreeNodeData);
297
320
  }
321
+ buildRowToDatumMap();
298
322
  return newTreeNodeData;
299
323
  },
300
324
  onSearchTree = async (value) => {
@@ -323,21 +347,27 @@ function TreeComponent(props) {
323
347
  },
324
348
 
325
349
  // utilities
326
- getNodeData = (itemId) => {
327
- function findNodeById(node, id) {
350
+ getNodeData = (id) => {
351
+ function findNodeById(node) {
328
352
  if (node.item.id === id) {
329
353
  return node;
330
354
  }
331
355
  if (!_.isEmpty(node.children)) {
332
- return _.find(node.children, (node2) => {
333
- return findNodeById(node2, id);
356
+ let found1 = null;
357
+ _.each(node.children, (node2) => {
358
+ const found2 = findNodeById(node2);
359
+ if (found2) {
360
+ found1 = found2;
361
+ return false; // break loop
362
+ }
334
363
  })
364
+ return found1
335
365
  }
336
366
  return false;
337
367
  }
338
368
  let found = null;
339
369
  _.each(getTreeNodeData(), (node) => {
340
- const foundNode = findNodeById(node, itemId);
370
+ const foundNode = findNodeById(node);
341
371
  if (foundNode) {
342
372
  found = foundNode;
343
373
  return false;
@@ -355,9 +385,9 @@ function TreeComponent(props) {
355
385
  datum = {
356
386
  item: treeNode,
357
387
  text: getNodeText(treeNode),
358
- iconCollapsed: getNodeIcon(treeNode, false),
359
- iconExpanded: getNodeIcon(treeNode, true),
360
- iconLeaf: getNodeIcon(treeNode),
388
+ iconCollapsed: getNodeIcon(COLLAPSED, treeNode),
389
+ iconExpanded: getNodeIcon(EXPANDED, treeNode),
390
+ iconLeaf: getNodeIcon(LEAF, treeNode),
361
391
  isExpanded: isRoot, // all non-root treeNodes are collapsed by default
362
392
  isVisible: isRoot ? areRootsVisible : true,
363
393
  isLoading: false,
@@ -385,7 +415,35 @@ function TreeComponent(props) {
385
415
  nodes = assembleDataTreeNodes();
386
416
  }
387
417
 
388
- setTreeNodeData(buildTreeNodeData(nodes));
418
+ const treeNodeData = buildTreeNodeData(nodes);
419
+ setTreeNodeData(treeNodeData);
420
+
421
+ buildRowToDatumMap();
422
+ },
423
+ buildRowToDatumMap = () => {
424
+ const rowToDatumMap = {};
425
+ let ix = 0;
426
+
427
+ function walkTree(datum) {
428
+ if (!datum.isVisible) {
429
+ return;
430
+ }
431
+
432
+ // Add this datum's id
433
+ rowToDatumMap[ix] = datum;
434
+ ix++;
435
+
436
+ if (datum.isExpanded) {
437
+ _.each(datum.children, (child) => {
438
+ walkTree(child);
439
+ });
440
+ }
441
+ }
442
+ _.each(getTreeNodeData(), (rootDatum) => {
443
+ walkTree(rootDatum);
444
+ });
445
+
446
+ setRowToDatumMap(rowToDatumMap);
389
447
  },
390
448
  datumContainsSelection = (datum) => {
391
449
  if (_.isEmpty(selection)) {
@@ -416,7 +474,7 @@ function TreeComponent(props) {
416
474
  });
417
475
  return found;
418
476
  }
419
- return searchChildren(treeNodeData);
477
+ return searchChildren(getTreeNodeData());
420
478
  },
421
479
  getTreeNodeByNodeId = (node_id) => {
422
480
  if (Repository) {
@@ -436,6 +494,27 @@ function TreeComponent(props) {
436
494
  });
437
495
  return ids;
438
496
  },
497
+ getDatumById = (id) => {
498
+
499
+ let found = null;
500
+
501
+ function walkTree(datum) {
502
+ if (datum.item.id === id) {
503
+ found = datum;
504
+ return;
505
+ }
506
+ _.each(datum.children, (child) => {
507
+ if (!found) {
508
+ walkTree(child);
509
+ }
510
+ });
511
+ }
512
+ _.each(getTreeNodeData(), (rootDatum) => {
513
+ walkTree(rootDatum);
514
+ });
515
+
516
+ return found;
517
+ },
439
518
  assembleDataTreeNodes = () => {
440
519
  // Populates the TreeNodes with .parent and .children references
441
520
  // NOTE: This is only for 'data', not for Repositories!
@@ -486,6 +565,25 @@ function TreeComponent(props) {
486
565
  Repository.areRootNodesLoaded = false;
487
566
  return buildAndSetTreeNodeData();
488
567
  },
568
+ reloadNode = async (node) => {
569
+ // mark node as loading
570
+ const existingDatum = getNodeData(node.id);
571
+ existingDatum.isLoading = true;
572
+ forceUpdate();
573
+
574
+ // reload from server
575
+ await node.reload();
576
+
577
+ // Refresh the node's display
578
+ const newDatum = buildTreeNodeDatum(node);
579
+
580
+ // copy the updated data to existingDatum
581
+ _.assign(existingDatum, _.omit(newDatum, ['isExpanded']));
582
+ existingDatum.isLoading = false;
583
+ forceUpdate();
584
+
585
+ buildRowToDatumMap();
586
+ },
489
587
  loadChildren = async (datum, depth = 1) => {
490
588
  // Show loading indicator (spinner underneath current node?)
491
589
  datum.isLoading = true;
@@ -508,12 +606,18 @@ function TreeComponent(props) {
508
606
  // Hide loading indicator
509
607
  datum.isLoading = false;
510
608
  forceUpdate();
609
+
610
+ buildRowToDatumMap();
511
611
  },
512
612
  collapseNodes = (nodes) => {
613
+ collapseNodesRecursive(nodes);
614
+ buildRowToDatumMap();
615
+ },
616
+ collapseNodesRecursive = (nodes) => {
513
617
  _.each(nodes, (node) => {
514
618
  node.isExpanded = false;
515
619
  if (!_.isEmpty(node.children)) {
516
- collapseNodes(node.children);
620
+ collapseNodesRecursive(node.children);
517
621
  }
518
622
  });
519
623
  },
@@ -561,9 +665,10 @@ function TreeComponent(props) {
561
665
 
562
666
  setSelection([currentNode]);
563
667
  scrollToNode(currentNode);
564
- highlightNode(currentNode);
668
+ setHighlitedDatum(currentDatum);
565
669
 
566
670
  setTreeNodeData(newTreeNodeData);
671
+ buildRowToDatumMap();
567
672
  },
568
673
  scrollToNode = (node) => {
569
674
  // Helper for expandPath
@@ -572,15 +677,6 @@ function TreeComponent(props) {
572
677
  // TODO: This will probably need different methods in web and mobile
573
678
 
574
679
 
575
- },
576
- highlightNode = (node) => {
577
- // Helper for expandPath
578
- // Show a brief highlight animation to draw attention to the node
579
-
580
- // TODO: This will probably need different methods in web and mobile
581
- // react-highlight for web?
582
-
583
-
584
680
  },
585
681
 
586
682
  // render
@@ -605,9 +701,11 @@ function TreeComponent(props) {
605
701
  if (canNodesReorder) {
606
702
  buttons.push({
607
703
  key: 'reorderBtn',
608
- text: 'Enter reorder mode',
609
- handler: () => setIsReorderMode(!isReorderMode),
610
- icon: isReorderMode ? NoReorderRows : ReorderRows,
704
+ text: (isDragMode ? 'Exit' : 'Enter') + ' reorder mode',
705
+ handler: () => {
706
+ setIsDragMode(!isDragMode)
707
+ },
708
+ icon: isDragMode ? NoReorderRows : ReorderRows,
611
709
  isDisabled: false,
612
710
  });
613
711
  }
@@ -652,7 +750,7 @@ function TreeComponent(props) {
652
750
  if (e.preventDefault && e.cancelable) {
653
751
  e.preventDefault();
654
752
  }
655
- if (isReorderMode) {
753
+ if (isDragMode) {
656
754
  return
657
755
  }
658
756
  switch (e.detail) {
@@ -676,7 +774,7 @@ function TreeComponent(props) {
676
774
  if (e.preventDefault && e.cancelable) {
677
775
  e.preventDefault();
678
776
  }
679
- if (isReorderMode) {
777
+ if (isDragMode) {
680
778
  return;
681
779
  }
682
780
 
@@ -692,7 +790,7 @@ function TreeComponent(props) {
692
790
  }
693
791
  }}
694
792
  flexDirection="row"
695
- ml={((areRootsVisible ? depth : depth -1) * 20) + 'px'}
793
+ ml={((areRootsVisible ? depth : depth -1) * DEPTH_INDENT_PX) + 'px'}
696
794
  >
697
795
  {({
698
796
  isHovered,
@@ -701,45 +799,52 @@ function TreeComponent(props) {
701
799
  }) => {
702
800
  let bg = nodeProps.bg || styles.TREE_NODE_BG,
703
801
  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;
802
+ if (!isDragMode) {
803
+ if (isSelected) {
804
+ if (showHovers && isHovered) {
805
+ mixWith = styles.TREE_NODE_SELECTED_HOVER_BG;
806
+ } else {
807
+ mixWith = styles.TREE_NODE_SELECTED_BG;
808
+ }
809
+ } else if (showHovers && isHovered) {
810
+ mixWith = styles.TREE_NODE_HOVER_BG;
709
811
  }
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);
812
+ if (mixWith) {
813
+ const
814
+ mixWithObj = nbToRgb(mixWith),
815
+ ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
816
+ bg = colourMixer.blend(bg, ratio, mixWithObj.color);
817
+ }
818
+ } else {
819
+
718
820
  }
719
821
  let WhichTreeNode = TreeNode,
720
- rowReorderProps = {};
721
- if (canNodesReorder && isReorderMode) {
722
- WhichTreeNode = ReorderableTreeNode;
723
- rowReorderProps = {
822
+ dragProps = {};
823
+ if (canNodesReorder && isDragMode && !datum.item.isRoot) { // Can't drag root nodes
824
+ WhichTreeNode = DraggableTreeNode;
825
+ dragProps = {
724
826
  mode: VERTICAL,
725
- onDragStart: onNodeReorderDragStart,
726
- onDrag: onNodeReorderDrag,
727
- onDragStop: onNodeReorderDragStop,
728
- proxyParent: treeRef.current?.getScrollableNode().children[0],
827
+ onDrag,
828
+ onDragStop,
829
+ getParentNode: (node) => node.parentElement.parentElement,
830
+ getDraggableNodeFromNode: (node) => node.parentElement,
831
+ getProxy: getDragProxy,
832
+ proxyParent: treeRef.current,
729
833
  proxyPositionRelativeToParent: true,
730
- getParentNode: (node) => node.parentElement.parentElement.parentElement,
731
- getProxy: getReorderProxy,
732
834
  };
835
+ nodeProps.width = '100%';
733
836
  }
734
837
 
735
838
  return <WhichTreeNode
736
839
  nodeProps={nodeProps}
840
+ {...dragProps}
737
841
  bg={bg}
738
842
  datum={datum}
739
843
  onToggle={onToggle}
844
+ isDragMode={isDragMode}
845
+ isHighlighted={highlitedDatum === datum}
740
846
 
741
847
  // fields={fields}
742
- {...rowReorderProps}
743
848
  />;
744
849
  }}
745
850
  </Pressable>;
@@ -762,129 +867,60 @@ function TreeComponent(props) {
762
867
  },
763
868
 
764
869
  // drag/drop
765
- getReorderProxy = (node) => {
870
+ getDragProxy = (node) => {
871
+
872
+ // TODO: Maybe the proxy should grab itself and all descendants??
873
+
766
874
  const
767
- row = node.parentElement.parentElement,
875
+ row = node,
768
876
  rowRect = row.getBoundingClientRect(),
769
877
  parent = row.parentElement,
770
878
  parentRect = parent.getBoundingClientRect(),
771
879
  proxy = row.cloneNode(true),
772
880
  top = rowRect.top - parentRect.top,
773
- dragNodeIx = Array.from(parent.children).indexOf(row)
881
+ rows = _.filter(parent.children, (childNode) => {
882
+ if (childNode.getBoundingClientRect().height === 0 && childNode.style.visibility !== 'hidden') {
883
+ return false; // Skip zero-height children
884
+ }
885
+ if (childNode === proxy) {
886
+ return false;
887
+ }
888
+ return true;
889
+ }),
890
+ dragRowIx = Array.from(rows).indexOf(row),
891
+ dragRowRecord = rowToDatumMap[dragRowIx].item;
774
892
 
775
- setDragNodeIx(dragNodeIx); // the ix of which record is being dragged
893
+ setDragNodeId(dragRowRecord.id); // the id of which record is being dragged
776
894
 
777
895
  proxy.style.top = top + 'px';
778
- proxy.style.left = '20px';
896
+ proxy.style.left = (dragRowRecord.depth * DEPTH_INDENT_PX) + 'px';
779
897
  proxy.style.height = rowRect.height + 'px';
780
898
  proxy.style.width = rowRect.width + 'px';
781
899
  proxy.style.display = 'flex';
782
- // proxy.style.backgroundColor = '#ccc';
783
900
  proxy.style.position = 'absolute';
784
- proxy.style.border = '1px solid #000';
901
+ proxy.style.border = '1px solid #bbb';
785
902
  return proxy;
786
903
  },
787
- onNodeReorderDragStart = (info, e, proxy, node) => {
788
- // console.log('onNodeReorderDragStart', info, e, proxy, node);
904
+ onDrag = (info, e, proxy, node) => {
905
+ // console.log('onDrag', info, e, proxy, node);
789
906
  const
790
907
  proxyRect = proxy.getBoundingClientRect(),
791
- row = node.parentElement.parentElement,
908
+ row = node,
792
909
  parent = row.parentElement,
793
910
  parentRect = parent.getBoundingClientRect(),
794
- rows = _.filter(row.parentElement.children, (childNode) => {
795
- return childNode.getBoundingClientRect().height !== 0; // Skip zero-height children
796
- }),
797
- currentY = proxyRect.top - parentRect.top, // top position of pointer, relative to page
798
- headerNodeIx = showHeaders ? 0 : null,
799
- firstActualNodeIx = showHeaders ? 1 : 0;
800
-
801
- // Figure out which index the user wants
802
- let newIx = 0;
803
- _.each(rows, (child, ix, all) => {
804
- const
805
- rect = child.getBoundingClientRect(), // rect of the row of this iteration
806
- {
807
- top,
808
- bottom,
809
- height,
810
- } = rect,
811
- compensatedTop = top - parentRect.top,
812
- compensatedBottom = bottom - parentRect.top,
813
- halfHeight = height / 2;
814
-
815
- if (ix === headerNodeIx || child === proxy) {
816
- return;
817
- }
818
- if (ix === firstActualNodeIx) {
819
- // first row
820
- if (currentY < compensatedTop + halfHeight) {
821
- newIx = firstActualNodeIx;
822
- return false;
823
- } else if (currentY < compensatedBottom) {
824
- newIx = firstActualNodeIx + 1;
825
- return false;
911
+ rows = _.filter(parent.children, (childNode) => {
912
+ if (childNode.getBoundingClientRect().height === 0 && childNode.style.visibility !== 'hidden') {
913
+ return false; // Skip zero-height children
826
914
  }
827
- return;
828
- } else if (ix === all.length -1) {
829
- // last row
830
- if (currentY < compensatedTop + halfHeight) {
831
- newIx = ix;
915
+ if (childNode === proxy) {
832
916
  return false;
833
917
  }
834
- newIx = ix +1;
835
- return false;
836
- }
837
-
838
- // all other rows
839
- if (compensatedTop <= currentY && currentY < compensatedTop + halfHeight) {
840
- newIx = ix;
841
- return false;
842
- } else if (currentY < compensatedBottom) {
843
- newIx = ix +1;
844
- return false;
845
- }
846
- });
847
-
848
- let useBottom = false;
849
- if (!rows[newIx] || rows[newIx] === proxy) {
850
- newIx--;
851
- useBottom = true;
852
- }
853
-
854
- // Render marker showing destination location
855
- const
856
- rowContainerRect = rows[newIx].getBoundingClientRect(),
857
- top = (useBottom ? rowContainerRect.bottom : rowContainerRect.top) - parentRect.top - parseInt(parent.style.borderWidth), // get relative Y position
858
- treeNodesContainer = treeRef.current._listRef._scrollRef.childNodes[0],
859
- treeNodesContainerRect = treeNodesContainer.getBoundingClientRect(),
860
- marker = document.createElement('div');
861
-
862
- marker.style.position = 'absolute';
863
- marker.style.top = top -4 + 'px'; // -4 so it's always visible
864
- marker.style.height = '4px';
865
- marker.style.width = treeNodesContainerRect.width + 'px';
866
- marker.style.backgroundColor = '#f00';
867
-
868
- treeNodesContainer.appendChild(marker);
869
-
870
- setDragNodeSlot({ ix: newIx, marker, useBottom, });
871
- },
872
- onNodeReorderDrag = (info, e, proxy, node) => {
873
- // console.log('onNodeReorderDrag', info, e, proxy, node);
874
- const
875
- proxyRect = proxy.getBoundingClientRect(),
876
- row = node.parentElement.parentElement,
877
- parent = row.parentElement,
878
- parentRect = parent.getBoundingClientRect(),
879
- rows = _.filter(row.parentElement.children, (childNode) => {
880
- return childNode.getBoundingClientRect().height !== 0; // Skip zero-height children
918
+ return true;
881
919
  }),
882
- currentY = proxyRect.top - parentRect.top, // top position of pointer, relative to page
883
- headerNodeIx = showHeaders ? 0 : null,
884
- firstActualNodeIx = showHeaders ? 1 : 0;
920
+ currentY = proxyRect.top - parentRect.top; // top position of pointer, relative to page
885
921
 
886
- // Figure out which index the user wants
887
- let newIx = 0;
922
+ // Figure out which row the user wants as a parentId
923
+ let newIx = 0; // default to root being new parentId
888
924
  _.each(rows, (child, ix, all) => {
889
925
  const
890
926
  rect = child.getBoundingClientRect(), // rect of the row of this iteration
@@ -893,117 +929,89 @@ function TreeComponent(props) {
893
929
  bottom,
894
930
  height,
895
931
  } = rect,
896
- compensatedTop = top - parentRect.top,
897
- compensatedBottom = bottom - parentRect.top,
898
- halfHeight = height / 2;
932
+ compensatedBottom = bottom - parentRect.top;
899
933
 
900
- if (ix === headerNodeIx || child === proxy) {
934
+ if (child === proxy) {
901
935
  return;
902
936
  }
903
- if (ix === firstActualNodeIx) {
937
+ if (ix === 0) {
904
938
  // first row
905
- if (currentY < compensatedTop + halfHeight) {
906
- newIx = firstActualNodeIx;
907
- return false;
908
- } else if (currentY < compensatedBottom) {
909
- newIx = firstActualNodeIx + 1;
939
+ if (currentY < compensatedBottom) {
940
+ newIx = 0;
910
941
  return false;
911
942
  }
912
943
  return;
913
944
  } else if (ix === all.length -1) {
914
945
  // last row
915
- if (currentY < compensatedTop + halfHeight) {
946
+ if (currentY < compensatedBottom) {
916
947
  newIx = ix;
917
948
  return false;
918
949
  }
919
- newIx = ix +1;
920
- return false;
950
+ return;
921
951
  }
922
952
 
923
953
  // all other rows
924
- if (compensatedTop <= currentY && currentY < compensatedTop + halfHeight) {
954
+ if (currentY < compensatedBottom) {
925
955
  newIx = ix;
926
956
  return false;
927
- } else if (currentY < compensatedBottom) {
928
- newIx = ix +1;
929
- return false;
930
957
  }
931
958
  });
932
959
 
933
- let useBottom = false;
934
- if (!rows[newIx] || rows[newIx] === proxy) {
935
- newIx--;
936
- useBottom = true;
937
- }
938
960
 
939
- // Render marker showing destination location (can't use regular render cycle because this div is absolutely positioned on page)
940
961
  const
941
- rowContainerRect = rows[newIx].getBoundingClientRect(),
942
- top = (useBottom ? rowContainerRect.bottom : rowContainerRect.top) - parentRect.top - parseInt(parent.style.borderWidth); // get relative Y position
943
- let marker = dragNodeSlot && dragNodeSlot.marker;
944
- if (marker) {
945
- marker.style.top = top -4 + 'px'; // -4 so it's always visible
946
- }
962
+ dragDatum = getDatumById(dragNodeId),
963
+ dragDatumChildIds = getDatumChildIds(dragDatum),
964
+ dropRowDatum = rowToDatumMap[newIx],
965
+ dropRowRecord = dropRowDatum.item,
966
+ dropNodeId = dropRowRecord.id,
967
+ dragNodeContainsDropNode = inArray(dropNodeId, dragDatumChildIds) || dropRowRecord.id === dragNodeId;
968
+
969
+ if (dragNodeContainsDropNode) {
970
+ // the node can be a child of any node except itself or its own descendants
971
+ setDropRowIx(null);
972
+ setHighlitedDatum(null);
973
+
974
+ } else {
975
+ console.log('setDropRowIx', newIx);
976
+ setDropRowIx(newIx);
947
977
 
948
- setDragNodeSlot({ ix: newIx, marker, useBottom, });
949
- // console.log('onNodeReorderDrag', newIx);
978
+ // highlight the drop node
979
+ setHighlitedDatum(dropRowDatum);
950
980
 
981
+ // shift proxy's depth
982
+ const depth = (dropRowRecord.id === dragNodeId) ? dropRowRecord.depth : dropRowRecord.depth + 1;
983
+ proxy.style.left = (depth * DEPTH_INDENT_PX) + 'px';
984
+ }
951
985
  },
952
- onNodeReorderDragStop = (delta, e, config) => {
953
- // console.log('onNodeReorderDragStop', delta, e, config);
954
- const
955
- dropIx = dragNodeSlot.ix,
956
- compensatedDragIx = showHeaders ? dragNodeIx -1 : dragNodeIx, // ix, without taking header row into account
957
- compensatedDropIx = showHeaders ? dropIx -1 : dropIx, // // ix, without taking header row into account
958
- dropPosition = dragNodeSlot.useBottom ? DROP_POSITION_AFTER : DROP_POSITION_BEFORE;
986
+ onDragStop = async (delta, e, config) => {
987
+ // console.log('onDragStop', delta, e, config);
959
988
 
960
- let shouldMove = true,
961
- finalDropIx = compensatedDropIx;
962
-
963
- if (dropPosition === DROP_POSITION_BEFORE) {
964
- if (dragNodeIx === dropIx || dragNodeIx === dropIx -1) { // basically before or after the drag row's origin
965
- // Same as origin; don't do anything
966
- shouldMove = false;
967
- } else {
968
- // Actually move it
969
- if (!Repository) { // If we're just going to be switching rows, rather than telling server to reorder rows, so maybe adjust finalDropIx...
970
- if (finalDropIx > compensatedDragIx) { // if we're dropping *before* the origin ix
971
- finalDropIx = finalDropIx -1; // Because we're using BEFORE, we want to switch with the row *prior to* the ix we're dropping before
972
- }
973
- }
974
- }
975
- } else if (dropPosition === DROP_POSITION_AFTER) {
976
- // Only happens on the very last row. Everything else is BEFORE...
977
- if (dragNodeIx === dropIx) {
978
- // Same as origin; don't do anything
979
- shouldMove = false;
980
- }
989
+ if (_.isNil(dropRowIx)) {
990
+ return;
981
991
  }
992
+
993
+ const
994
+ dragDatum = getDatumById(dragNodeId),
995
+ dragRowRecord = dragDatum.item,
996
+ dropRowDatum = rowToDatumMap[dropRowIx],
997
+ dropRowRecord = dropRowDatum.item;
982
998
 
983
- if (shouldMove) {
984
- // Update the row with the new ix
985
- let dragRecord,
986
- dropRecord;
987
- if (Repository) {
988
- dragRecord = Repository.getByIx(compensatedDragIx);
989
- dropRecord = Repository.getByIx(finalDropIx);
990
-
991
- Repository.reorder(dragRecord, dropRecord, dropPosition);
999
+ if (Repository) {
1000
+
1001
+ const commonAncestorId = await Repository.moveTreeNode(dragRowRecord, dropRowRecord.id);
1002
+ const commonAncestorDatum = getDatumById(commonAncestorId);
1003
+ reloadNode(commonAncestorDatum.item);
992
1004
 
993
- } else {
994
- function arrayMove(arr, fromIndex, toIndex) {
995
- var element = arr[fromIndex];
996
- arr.splice(fromIndex, 1);
997
- arr.splice(toIndex, 0, element);
998
- }
999
- arrayMove(data, compensatedDragIx, finalDropIx);
1000
- }
1001
- }
1005
+ } else {
1002
1006
 
1003
- if (dragNodeSlot) {
1004
- dragNodeSlot.marker.remove();
1007
+ throw Error('Not yet implemented');
1008
+ // function arrayMove(arr, fromIndex, toIndex) {
1009
+ // var element = arr[fromIndex];
1010
+ // arr.splice(fromIndex, 1);
1011
+ // arr.splice(toIndex, 0, element);
1012
+ // }
1013
+ // arrayMove(data, dragNodeIx, finalDropIx);
1005
1014
  }
1006
- setDragNodeSlot(null);
1007
1015
  };
1008
1016
 
1009
1017
  useEffect(() => {
@@ -1070,8 +1078,8 @@ function TreeComponent(props) {
1070
1078
  });
1071
1079
 
1072
1080
  const
1073
- headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue, getTreeNodeData()]),
1074
- footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode, getTreeNodeData()]);
1081
+ headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue, isDragMode, getTreeNodeData()]),
1082
+ footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isDragMode, getTreeNodeData()]);
1075
1083
 
1076
1084
  if (!isReady) {
1077
1085
  return null;
@@ -1088,7 +1096,17 @@ function TreeComponent(props) {
1088
1096
  treeFooterComponent = <Toolbar>{footerToolbarItemComponents}</Toolbar>;
1089
1097
  }
1090
1098
  }
1091
-
1099
+
1100
+ const borderProps = {};
1101
+ if (isDragMode) {
1102
+ borderProps.borderWidth = isDragMode ? styles.REORDER_BORDER_WIDTH : 0;
1103
+ borderProps.borderColor = isDragMode ? styles.REORDER_BORDER_COLOR : null;
1104
+ borderProps.borderStyle = styles.REORDER_BORDER_STYLE;
1105
+ } else {
1106
+ borderProps.borderTopWidth = isLoading ? 2 : 1;
1107
+ borderProps.borderTopColor = isLoading ? '#f00' : 'trueGray.300';
1108
+ }
1109
+
1092
1110
  return <>
1093
1111
  <Column
1094
1112
  {...testProps('Tree')}
@@ -1098,11 +1116,18 @@ function TreeComponent(props) {
1098
1116
  {topToolbar}
1099
1117
  {headerToolbarItemComponents?.length && <Row>{headerToolbarItemComponents}</Row>}
1100
1118
 
1101
- <Column w="100%" flex={1} p={2} borderTopWidth={isLoading ? 2 : 1} borderTopColor={isLoading ? '#f00' : 'trueGray.300'} onClick={() => {
1102
- if (!isReorderMode) {
1103
- deselectAll();
1104
- }
1105
- }}>
1119
+ <Column
1120
+ ref={treeRef}
1121
+ w="100%"
1122
+ flex={1}
1123
+ p={2}
1124
+ {...borderProps}
1125
+ onClick={() => {
1126
+ if (!isDragMode) {
1127
+ deselectAll();
1128
+ }
1129
+ }}
1130
+ >
1106
1131
  {!treeNodes?.length ? <NoRecordsFound text={noneFoundText} onRefresh={reloadTree} /> :
1107
1132
  treeNodes}
1108
1133
  </Column>
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, } from 'react';
1
+ import { useMemo, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  Icon,
@@ -6,9 +6,6 @@ import {
6
6
  Spinner,
7
7
  Text,
8
8
  } from 'native-base';
9
- import {
10
- VERTICAL,
11
- } from '../../Constants/Directions.js';
12
9
  import UiGlobals from '../../UiGlobals.js';
13
10
  import withDraggable from '../Hoc/withDraggable.js';
14
11
  import IconButton from '../Buttons/IconButton.js';
@@ -22,6 +19,8 @@ export default function TreeNode(props) {
22
19
  bg,
23
20
  datum,
24
21
  onToggle,
22
+ isDragMode,
23
+ isHighlighted,
25
24
  ...propsToPass
26
25
  } = props,
27
26
  styles = UiGlobals.styles,
@@ -37,7 +36,9 @@ export default function TreeNode(props) {
37
36
  iconLeaf = datum.iconLeaf,
38
37
  hash = item?.hash || item;
39
38
 
40
- const icon = hasChildren ? (isExpanded ? iconExpanded : iconCollapsed) : iconLeaf;
39
+ const
40
+ icon = hasChildren ? (isExpanded ? iconExpanded : iconCollapsed) : iconLeaf,
41
+ adjustedBg = isHighlighted ? styles.TREE_NODE_HIGHLIGHTED_BG : bg;
41
42
 
42
43
  return useMemo(() => {
43
44
 
@@ -45,14 +46,14 @@ export default function TreeNode(props) {
45
46
  alignItems="center"
46
47
  flexGrow={1}
47
48
  {...nodeProps}
48
- bg={bg}
49
+ bg={adjustedBg}
49
50
  key={hash}
50
51
  >
51
52
  {isPhantom && <Box position="absolute" bg="#f00" h={2} w={2} t={0} l={0} />}
52
53
 
53
54
  {isLoading ?
54
55
  <Spinner px={2} /> :
55
- (hasChildren ? <IconButton icon={icon} onPress={() => onToggle(datum)} /> : <Icon as={icon} px={2} />)}
56
+ (hasChildren && !isDragMode ? <IconButton icon={icon} onPress={() => onToggle(datum)} /> : <Icon as={icon} px={2} />)}
56
57
 
57
58
  <Text
58
59
  overflow="hidden"
@@ -70,7 +71,7 @@ export default function TreeNode(props) {
70
71
  </Row>;
71
72
  }, [
72
73
  nodeProps,
73
- bg,
74
+ adjustedBg,
74
75
  item,
75
76
  isPhantom,
76
77
  hash, // this is an easy way to determine if the data has changed and the item needs to be rerendered
@@ -81,16 +82,9 @@ export default function TreeNode(props) {
81
82
  icon,
82
83
  onToggle,
83
84
  isLoading,
85
+ isDragMode,
86
+ isHighlighted,
84
87
  ]);
85
88
  }
86
89
 
87
- function withAdditionalProps(WrappedComponent) {
88
- return (props) => {
89
- return <WrappedComponent
90
- mode={VERTICAL}
91
- {...props}
92
- />;
93
- };
94
- }
95
-
96
- export const ReorderableTreeNode = withAdditionalProps(withDraggable(TreeNode));
90
+ export const DraggableTreeNode = withDraggable(TreeNode);
@@ -1,5 +1,6 @@
1
1
  export const HORIZONTAL = 'HORIZONTAL';
2
2
  export const VERTICAL = 'VERTICAL';
3
+ export const XY = 'XY';
3
4
  export const LEFT = 'LEFT';
4
5
  export const RIGHT = 'RIGHT';
5
6
  export const NORTH = 'NORTH';
@@ -76,6 +76,9 @@ const defaults = {
76
76
  PANEL_HEADER_TEXT_FONTSIZE: 15,
77
77
  PANEL_HEADER_PX: 3,
78
78
  PANEL_HEADER_PY: 1,
79
+ REORDER_BORDER_COLOR: '#23d9ea',
80
+ REORDER_BORDER_WIDTH: 4,
81
+ REORDER_BORDER_STYLE: 'dashed',
79
82
  TAB_ACTIVE_BG: 'trueGray.200',
80
83
  TAB_ACTIVE_HOVER_BG: 'trueGray.200',
81
84
  TAB_ACTIVE_COLOR: 'primary.800',
@@ -94,6 +97,7 @@ const defaults = {
94
97
  TREE_NODE_HOVER_BG: 'hover',
95
98
  TREE_NODE_SELECTED_BG: 'selected',
96
99
  TREE_NODE_SELECTED_HOVER_BG: 'selectedHover',
100
+ TREE_NODE_HIGHLIGHTED_BG: '#ffa',
97
101
  TOOLBAR_ITEMS_COLOR: 'trueGray.800',
98
102
  TOOLBAR_ITEMS_DISABLED_COLOR: 'trueGray.400',
99
103
  TOOLBAR_ITEMS_ICON_SIZE: 'sm',
@@ -1,4 +1,7 @@
1
1
  export const SORT_ASCENDING = 'ASC'; // This is what RestTrait expects
2
2
  export const SORT_DESCENDING = 'DESC'; // This is what RestTrait expects
3
3
  export const DROP_POSITION_BEFORE = 'before'; // This is what RestTrait expects
4
- export const DROP_POSITION_AFTER = 'after'; // This is what RestTrait expects
4
+ export const DROP_POSITION_AFTER = 'after'; // This is what RestTrait expects
5
+ export const COLLAPSED = 'COLLAPSED';
6
+ export const EXPANDED = 'EXPANDED';
7
+ export const LEAF = 'LEAF';
@@ -5,8 +5,8 @@ export const UI_MODE_WEB = 'Web';
5
5
  export const UI_MODE_REACT_NATIVE = 'ReactNative';
6
6
 
7
7
  export let CURRENT_MODE;
8
- if (isReactNative) {
9
- CURRENT_MODE = UI_MODE_REACT_NATIVE;
10
- } else if (isBrowser || isWebWorker) {
8
+ if (isBrowser || isWebWorker) {
11
9
  CURRENT_MODE = UI_MODE_WEB;
10
+ } else if (isReactNative) {
11
+ CURRENT_MODE = UI_MODE_REACT_NATIVE;
12
12
  }