@onehat/ui 0.4.64 → 0.4.66

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.
@@ -21,12 +21,18 @@ import {
21
21
  EXPANDED,
22
22
  LEAF,
23
23
  } from '../../Constants/Tree.js';
24
+ import {
25
+ UI_MODE_WEB,
26
+ UI_MODE_NATIVE,
27
+ CURRENT_MODE,
28
+ } from '../../Constants/UiModes.js';
24
29
  import UiGlobals from '../../UiGlobals.js';
25
30
  import useForceUpdate from '../../Hooks/useForceUpdate.js';
26
31
  import withContextMenu from '../Hoc/withContextMenu.js';
27
32
  import withAlert from '../Hoc/withAlert.js';
28
33
  import withComponent from '../Hoc/withComponent.js';
29
34
  import withData from '../Hoc/withData.js';
35
+ import { withDropTarget } from '../Hoc/withDnd.js';
30
36
  import withEvents from '../Hoc/withEvents.js';
31
37
  import withSideEditor from '../Hoc/withSideEditor.js';
32
38
  import withFilters from '../Hoc/withFilters.js';
@@ -41,7 +47,7 @@ import inArray from '../../Functions/inArray.js';
41
47
  import testProps from '../../Functions/testProps.js';
42
48
  import CenterBox from '../Layout/CenterBox.js';
43
49
  import ReloadButton from '../Buttons/ReloadButton.js';
44
- import TreeNode, { DraggableTreeNode } from './TreeNode.js';
50
+ import TreeNode, { DragSourceDropTargetTreeNode, DragSourceTreeNode, DropTargetTreeNode } from './TreeNode.js';
45
51
  import FormPanel from '../Panel/FormPanel.js';
46
52
  import Input from '../Form/Field/Input.js';
47
53
  import Xmark from '../Icons/Xmark.js';
@@ -52,8 +58,6 @@ import FolderClosed from '../Icons/FolderClosed.js';
52
58
  import FolderOpen from '../Icons/FolderOpen.js';
53
59
  import Gear from '../Icons/Gear.js';
54
60
  import MagnifyingGlass from '../Icons/MagnifyingGlass.js';
55
- import NoReorderRows from '../Icons/NoReorderRows.js';
56
- import ReorderRows from '../Icons/ReorderRows.js';
57
61
  import PaginationToolbar from '../Toolbar/PaginationToolbar.js';
58
62
  import NoRecordsFound from '../Grid/NoRecordsFound.js';
59
63
  import Toolbar from '../Toolbar/Toolbar.js';
@@ -68,6 +72,8 @@ const
68
72
  DOUBLE_CLICK = 2,
69
73
  TRIPLE_CLICK = 3;
70
74
 
75
+ // NOTE: If using TreeComponent with getCustomDragProxy, ensure that <GlobalDragProxy /> exists in App.js
76
+
71
77
  function TreeComponent(props) {
72
78
  const {
73
79
  areRootsVisible = true,
@@ -110,7 +116,21 @@ function TreeComponent(props) {
110
116
  disableLoadingIndicator = false,
111
117
  disableSelectorSelected = false,
112
118
  showHovers = true,
113
- canNodesReorder = false,
119
+ showSelectHandle = true,
120
+ isNodeSelectable = true,
121
+ isNodeHoverable = true,
122
+ canNodesMoveInternally = false,
123
+ canNodeMoveInternally, // optional fn to customize whether each node can be dragged INternally
124
+ canNodeMoveExternally, // optional fn to customize whether each node can be dragged EXternally
125
+ canNodeAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
126
+ getCustomDragProxy, // optional fn to render custom drag preview: (item, selection) => ReactElement
127
+ dragPreviewOptions, // optional object for drag preview positioning options
128
+ areNodesDragSource = false,
129
+ nodeDragSourceType,
130
+ getNodeDragSourceItem,
131
+ areNodesDropTarget = false,
132
+ dropTargetAccept,
133
+ onNodeDrop,
114
134
  allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the node clicked on
115
135
  disableBottomToolbar = false,
116
136
  bottomToolbar = null,
@@ -161,6 +181,12 @@ function TreeComponent(props) {
161
181
  // withPermissions
162
182
  canUser,
163
183
 
184
+ // withDnd
185
+ isDropTarget,
186
+ canDrop,
187
+ isOver,
188
+ dropTargetRef,
189
+
164
190
  // withSelection
165
191
  selection,
166
192
  setSelection,
@@ -173,19 +199,15 @@ function TreeComponent(props) {
173
199
  noSelectorMeansNoResults = false,
174
200
 
175
201
  } = props,
176
- styles = UiGlobals.styles,
177
202
  forceUpdate = useForceUpdate(),
178
203
  treeRef = useRef(),
179
204
  treeNodeData = useRef(),
205
+ dragSelectionRef = useRef([]),
180
206
  [isReady, setIsReady] = useState(false),
181
207
  [isLoading, setIsLoading] = useState(false),
182
- [rowToDatumMap, setRowToDatumMap] = useState({}),
183
208
  [searchResults, setSearchResults] = useState([]),
184
209
  [searchFormData, setSearchFormData] = useState([]),
185
210
  [highlitedDatum, setHighlitedDatum] = useState(null),
186
- [isDragMode, setIsDragMode] = useState(false),
187
- [dragNodeId, setDragNodeId] = useState(null),
188
- [dropRowIx, setDropRowIx] = useState(null),
189
211
  [treeSearchValue, setTreeSearchValue] = useState(''),
190
212
 
191
213
  // state getters & setters
@@ -270,7 +292,7 @@ function TreeComponent(props) {
270
292
  }
271
293
  const
272
294
  parent = selection[0],
273
- parentDatum = getNodeData(parent.id);
295
+ parentDatum = getDatumById(parent.id);
274
296
 
275
297
  if (parent.hasChildren && !parent.areChildrenLoaded) {
276
298
  await loadChildren(parentDatum);
@@ -284,7 +306,7 @@ function TreeComponent(props) {
284
306
  // Add the entity to the tree, show parent as hasChildren and expanded
285
307
  const
286
308
  parent = selection[0],
287
- parentDatum = getNodeData(parent.id);
309
+ parentDatum = getDatumById(parent.id);
288
310
  if (!parent.hasChildren) {
289
311
  parent.hasChildren = true; // since we're adding a new child
290
312
  }
@@ -292,7 +314,6 @@ function TreeComponent(props) {
292
314
  parentDatum.isExpanded = true;
293
315
  }
294
316
 
295
- buildRowToDatumMap();
296
317
  forceUpdate();
297
318
  },
298
319
  onAfterAddSave = (entities) => {
@@ -307,7 +328,7 @@ function TreeComponent(props) {
307
328
  // Refresh the node's display
308
329
  const
309
330
  node = entities[0],
310
- existingDatum = getNodeData(node.id), // TODO: Make this work for >1 entity
331
+ existingDatum = getDatumById(node.id), // TODO: Make this work for >1 entity
311
332
  newDatum = buildTreeNodeDatum(node);
312
333
 
313
334
  // copy the updated data to existingDatum
@@ -317,7 +338,7 @@ function TreeComponent(props) {
317
338
 
318
339
  if (node.parent?.id) {
319
340
  const
320
- existingParentDatum = getNodeData(node.parent.id),
341
+ existingParentDatum = getDatumById(node.parent.id),
321
342
  newParentDatum = buildTreeNodeDatum(node.parent);
322
343
  _.assign(existingParentDatum, newParentDatum);
323
344
  existingParentDatum.isExpanded = true;
@@ -331,7 +352,7 @@ function TreeComponent(props) {
331
352
  onBeforeSave = (entities) => {
332
353
  const
333
354
  node = entities[0],
334
- datum = getNodeData(node.id); // TODO: Make this work for >1 entity
355
+ datum = getDatumById(node.id); // TODO: Make this work for >1 entity
335
356
 
336
357
  datum.isLoading = true;
337
358
  forceUpdate();
@@ -339,9 +360,7 @@ function TreeComponent(props) {
339
360
  onAfterDelete = async (entities) => {
340
361
  const parent = entities[0].parent;
341
362
  if (parent) {
342
- await reloadNode(parent); // includes buildRowToDatumMap
343
- } else {
344
- buildRowToDatumMap();
363
+ await reloadNode(parent);
345
364
  }
346
365
  },
347
366
  onToggle = async (datum, e) => {
@@ -376,7 +395,6 @@ function TreeComponent(props) {
376
395
  }
377
396
 
378
397
  forceUpdate();
379
- buildRowToDatumMap();
380
398
  },
381
399
  onCollapseAll = () => {
382
400
  const newTreeNodeData = _.clone(getTreeNodeData());
@@ -473,35 +491,50 @@ function TreeComponent(props) {
473
491
  });
474
492
  },
475
493
 
476
- // utilities
477
- getNodeData = (id) => {
478
- function findNodeById(node) {
479
- if (node.item.id === id) {
480
- return node;
481
- }
482
- if (!_.isEmpty(node.children)) {
483
- let found1 = null;
484
- _.each(node.children, (node2) => {
485
- const found2 = findNodeById(node2);
486
- if (found2) {
487
- found1 = found2;
488
- return false; // break loop
489
- }
490
- })
491
- return found1
492
- }
493
- return false;
494
+ // internal DND
495
+ onInternalNodeDrop = async (droppedOn, droppedItem) => {
496
+ let selectedNodes = [];
497
+ if (droppedItem.getSelection) {
498
+ selectedNodes = droppedItem.getSelection();
494
499
  }
495
- let found = null;
496
- _.each(getTreeNodeData(), (node) => {
497
- const foundNode = findNodeById(node);
498
- if (foundNode) {
499
- found = foundNode;
500
- return false;
501
- }
500
+ if (_.isEmpty(selectedNodes)) {
501
+ selectedNodes = [droppedItem.item];
502
+ }
503
+
504
+ // filter out nodes that would already be moved by others in the selection
505
+ const selectedNodesClone = [...selectedNodes];
506
+ selectedNodes = selectedNodes.filter((node) => {
507
+ let isDescendant = false;
508
+ _.each(selectedNodesClone, (otherNode) => {
509
+ if (node.id === otherNode.id) {
510
+ return false; // skip self
511
+ }
512
+ isDescendant = isDescendantOf(node, otherNode);
513
+ if (isDescendant) {
514
+ return false; // found descendant; break loop
515
+ }
516
+ isDescendant = isDescendantOf(otherNode, node);
517
+ if (isDescendant) {
518
+ return false; // found ancestor; break loop
519
+ }
520
+ });
521
+ return !isDescendant;
502
522
  });
503
- return found;
523
+
524
+ const isMultiSelection = selectedNodes.length > 1;
525
+ if (isMultiSelection) {
526
+ alert('moving multiple disparate nodes not yet implemented');
527
+ return;
528
+ }
529
+
530
+ const selectedNode = selectedNodes[0];
531
+ const commonAncestorId = await Repository.moveTreeNode(selectedNode, droppedOn.id);
532
+ const commonAncestorDatum = getDatumById(commonAncestorId);
533
+ reloadNode(commonAncestorDatum.item);
534
+
504
535
  },
536
+
537
+ // utilities
505
538
  buildTreeNodeDatum = (treeNode, defaultToExpanded = false) => {
506
539
  // Build the data-representation of one node and its children,
507
540
  // caching text & icon, keeping track of the state for whole tree
@@ -511,6 +544,7 @@ function TreeComponent(props) {
511
544
  children = buildTreeNodeData(treeNode.children, defaultToExpanded), // recursively get data for children
512
545
  datum = {
513
546
  item: treeNode,
547
+ treeRef,
514
548
  text: getNodeText(treeNode),
515
549
  content: getNodeContent ? getNodeContent(treeNode) : null,
516
550
  iconCollapsed: getNodeIcon(COLLAPSED, treeNode),
@@ -548,38 +582,11 @@ function TreeComponent(props) {
548
582
  const treeNodeData = buildTreeNodeData(nodes);
549
583
  setTreeNodeData(treeNodeData);
550
584
 
551
- buildRowToDatumMap();
552
-
553
585
  if (onTreeLoad) {
554
586
  onTreeLoad(self);
555
587
  }
556
588
  return treeNodeData;
557
589
  },
558
- buildRowToDatumMap = () => {
559
- const rowToDatumMap = {};
560
- let ix = 0;
561
-
562
- function walkTree(datum) {
563
- if (!datum.isVisible) {
564
- return;
565
- }
566
-
567
- // Add this datum's id
568
- rowToDatumMap[ix] = datum;
569
- ix++;
570
-
571
- if (datum.isExpanded) {
572
- _.each(datum.children, (child) => {
573
- walkTree(child);
574
- });
575
- }
576
- }
577
- _.each(getTreeNodeData(), (rootDatum) => {
578
- walkTree(rootDatum);
579
- });
580
-
581
- setRowToDatumMap(rowToDatumMap);
582
- },
583
590
  datumContainsSelection = (datum) => {
584
591
  if (_.isEmpty(selection)) {
585
592
  return false;
@@ -698,13 +705,38 @@ function TreeComponent(props) {
698
705
 
699
706
  return treeNodes;
700
707
  },
708
+ belongsToThisTree = (treeNode) => {
709
+ if (!treeNode) {
710
+ return false;
711
+ }
712
+ const datum = getDatumById(treeNode.id);
713
+ if (!datum) {
714
+ return false;
715
+ }
716
+ return datum.treeRef === treeRef;
717
+ },
718
+ isDescendantOf = (potentialDescendant, potentialAncestor) => {
719
+ // Check if potentialDescendant is a descendant of potentialAncestor
720
+ // by walking up the parent chain from potentialDescendant
721
+ let currentTreeNode = potentialDescendant;
722
+ while(currentTreeNode) {
723
+ if (currentTreeNode.id === potentialAncestor.id) {
724
+ return true;
725
+ }
726
+ currentTreeNode = currentTreeNode.parent;
727
+ }
728
+ return false;
729
+ },
730
+ isChildOf = (potentialChild, potentialParent) => {
731
+ return potentialChild.parent?.id === potentialParent.id;
732
+ },
701
733
  reloadTree = () => {
702
734
  Repository.areRootNodesLoaded = false;
703
735
  return buildAndSetTreeNodeData();
704
736
  },
705
737
  reloadNode = async (node) => {
706
738
  // mark node as loading
707
- const existingDatum = getNodeData(node.id);
739
+ const existingDatum = getDatumById(node.id);
708
740
  existingDatum.isLoading = true;
709
741
  forceUpdate();
710
742
 
@@ -718,8 +750,6 @@ function TreeComponent(props) {
718
750
  _.assign(existingDatum, _.omit(newDatum, ['isExpanded']));
719
751
  existingDatum.isLoading = false;
720
752
  forceUpdate();
721
-
722
- buildRowToDatumMap();
723
753
  },
724
754
  loadChildren = async (datum, depth = 1) => {
725
755
 
@@ -755,12 +785,9 @@ function TreeComponent(props) {
755
785
  // Hide loading indicator
756
786
  datum.isLoading = false;
757
787
  forceUpdate();
758
-
759
- buildRowToDatumMap();
760
788
  },
761
789
  collapseNodes = (nodes) => {
762
790
  collapseNodesRecursive(nodes);
763
- buildRowToDatumMap();
764
791
  },
765
792
  collapseNodesRecursive = (nodes) => {
766
793
  _.each(nodes, (node) => {
@@ -779,7 +806,6 @@ function TreeComponent(props) {
779
806
 
780
807
  // expand them in UI
781
808
  expandNodesRecursive(nodes);
782
- buildRowToDatumMap();
783
809
  },
784
810
  expandNodesRecursive = (nodes) => {
785
811
  _.each(nodes, (node) => {
@@ -846,7 +872,6 @@ function TreeComponent(props) {
846
872
  }
847
873
 
848
874
  setTreeNodeData(newTreeNodeData);
849
- buildRowToDatumMap();
850
875
  },
851
876
  scrollToNode = (node) => {
852
877
  // Helper for expandPath
@@ -907,17 +932,6 @@ function TreeComponent(props) {
907
932
  isDisabled: false,
908
933
  },
909
934
  ];
910
- if (canNodesReorder) {
911
- buttons.push({
912
- key: 'reorderBtn',
913
- text: (isDragMode ? 'Exit' : 'Enter') + ' reorder mode',
914
- handler: () => {
915
- setIsDragMode(!isDragMode);
916
- },
917
- icon: isDragMode ? NoReorderRows : ReorderRows,
918
- isDisabled: false,
919
- });
920
- }
921
935
  if (isNodeTextConfigurable && editDisplaySettings) {
922
936
  buttons.push({
923
937
  key: 'editNodeTextBtn',
@@ -978,9 +992,6 @@ function TreeComponent(props) {
978
992
  if (e.preventDefault && e.cancelable) {
979
993
  e.preventDefault();
980
994
  }
981
- if (isDragMode) {
982
- return
983
- }
984
995
  switch (e.detail) {
985
996
  case SIMULATED_CLICK:
986
997
  case SINGLE_CLICK:
@@ -1014,9 +1025,6 @@ function TreeComponent(props) {
1014
1025
  if (e.preventDefault && e.cancelable) {
1015
1026
  e.preventDefault();
1016
1027
  }
1017
- if (isDragMode) {
1018
- return;
1019
- }
1020
1028
 
1021
1029
  if (!setSelection) {
1022
1030
  return;
@@ -1030,6 +1038,8 @@ function TreeComponent(props) {
1030
1038
  }
1031
1039
  }}
1032
1040
  className={`
1041
+ Pressable
1042
+ Node
1033
1043
  flex-row
1034
1044
  `}
1035
1045
  style={{
@@ -1041,32 +1051,176 @@ function TreeComponent(props) {
1041
1051
  focused,
1042
1052
  pressed,
1043
1053
  }) => {
1044
- let WhichTreeNode = TreeNode,
1045
- dragProps = {};
1046
- if (canNodesReorder && isDragMode && !datum.item.isRoot) { // Can't drag root nodes
1047
- WhichTreeNode = DraggableTreeNode;
1048
- dragProps = {
1049
- mode: VERTICAL,
1050
- onDrag,
1051
- onDragStop,
1052
- getParentNode: (node) => node.parentElement.parentElement,
1053
- getDraggableNodeFromNode: (node) => node.parentElement,
1054
- getProxy: getDragProxy,
1055
- proxyParent: treeRef.current,
1056
- proxyPositionRelativeToParent: true,
1057
- };
1058
- nodeProps.className = 'w-full';
1054
+ const nodeDragProps = {};
1055
+ let WhichNode = TreeNode;
1056
+ if (CURRENT_MODE === UI_MODE_WEB) { // DND is currently web-only TODO: implement for RN
1057
+ // Create a method that gets an always-current copy of the selection ids
1058
+ dragSelectionRef.current = selection;
1059
+ const getSelection = () => dragSelectionRef.current;
1060
+
1061
+ const userHasPermissionToDrag = (!canUser || canUser(EDIT));
1062
+ if (userHasPermissionToDrag) {
1063
+ // NOTE: The Tree can either drag nodes internally or externally, but not both at the same time!
1064
+
1065
+ // assign event handlers
1066
+ if (canNodesMoveInternally) {
1067
+ // internal drag/drop
1068
+ const nodeDragSourceType = 'internal';
1069
+ WhichNode = DragSourceDropTargetTreeNode;
1070
+ nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
1071
+ nodeDragProps.dragSourceType = nodeDragSourceType;
1072
+ nodeDragProps.dragSourceItem = {
1073
+ id: item.id,
1074
+ item,
1075
+ getSelection,
1076
+ type: nodeDragSourceType,
1077
+ };
1078
+
1079
+ // Prevent root nodes from being dragged, and use custom logic if provided
1080
+ nodeDragProps.canDrag = (monitor) => {
1081
+ const currentSelection = getSelection();
1082
+
1083
+ // Check if any selected node is a root node (can't drag root nodes)
1084
+ const hasRootNode = currentSelection.some(node => node.isRoot);
1085
+ if (hasRootNode) {
1086
+ return false;
1087
+ }
1088
+
1089
+ // Use custom drag validation if provided
1090
+ if (canNodeMoveInternally) {
1091
+ // In multi-selection, all nodes must be draggable
1092
+ return currentSelection.every(node => canNodeMoveInternally(node));
1093
+ }
1094
+
1095
+ return true;
1096
+ };
1097
+
1098
+ // Add custom drag preview options
1099
+ if (dragPreviewOptions) {
1100
+ nodeDragProps.dragPreviewOptions = dragPreviewOptions;
1101
+ }
1102
+
1103
+ // Add drag preview rendering
1104
+ nodeDragProps.getDragProxy = getCustomDragProxy ?
1105
+ (dragItem) => getCustomDragProxy(item, getSelection()) :
1106
+ null; // Let GlobalDragProxy handle the default case
1107
+
1108
+ const dropTargetAccept = 'internal';
1109
+ nodeDragProps.isDropTarget = true;
1110
+ nodeDragProps.dropTargetAccept = dropTargetAccept;
1111
+
1112
+ // Define validation logic once for reuse
1113
+ const validateDrop = (droppedItem) => {
1114
+ if (!droppedItem) {
1115
+ return false;
1116
+ }
1117
+
1118
+ const currentSelection = getSelection();
1119
+
1120
+ // Always include the dragged item itself in validation
1121
+ // If no selection exists, the dragged item is what we're moving
1122
+ const nodesToValidate = currentSelection.length > 0 ? currentSelection : [droppedItem.item];
1123
+
1124
+ // validate that the dropped item is not already a direct child of the target node
1125
+ if (isChildOf(droppedItem.item, item)) {
1126
+ return false;
1127
+ }
1128
+
1129
+ // Validate that none of the nodes being moved can be dropped into the target location
1130
+ for (const nodeToMove of nodesToValidate) {
1131
+ if (nodeToMove.id === item.id) {
1132
+ // Cannot drop a node onto itself
1133
+ return false;
1134
+ }
1135
+ if (isDescendantOf(item, nodeToMove)) {
1136
+ // Cannot drop a node into its own descendants
1137
+ return false;
1138
+ }
1139
+ }
1140
+
1141
+ if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1142
+ // custom business logic
1143
+ return canNodeAcceptDrop(item, droppedItem);
1144
+ }
1145
+ return true;
1146
+ };
1147
+
1148
+ // Use the validation function for React DnD
1149
+ nodeDragProps.canDrop = (droppedItem, monitor) => validateDrop(droppedItem);
1150
+
1151
+ // Pass the same validation function for visual feedback
1152
+ nodeDragProps.validateDrop = validateDrop;
1153
+
1154
+ nodeDragProps.onDrop = (droppedItem) => {
1155
+ if (belongsToThisTree(droppedItem)) {
1156
+ onInternalNodeDrop(item, droppedItem);
1157
+ }
1158
+ };
1159
+ } else {
1160
+ // external drag/drop
1161
+ if (areNodesDragSource) {
1162
+ WhichNode = DragSourceTreeNode;
1163
+ nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
1164
+ nodeDragProps.dragSourceType = nodeDragSourceType;
1165
+ if (getNodeDragSourceItem) {
1166
+ nodeDragProps.dragSourceItem = getNodeDragSourceItem(item, getSelection, nodeDragSourceType);
1167
+ } else {
1168
+ nodeDragProps.dragSourceItem = {
1169
+ id: item.id,
1170
+ getSelection,
1171
+ type: nodeDragSourceType,
1172
+ };
1173
+ }
1174
+ if (canNodeMoveExternally) {
1175
+ nodeDragProps.canDrag = canNodeMoveExternally;
1176
+ }
1177
+
1178
+ // Add custom drag preview options
1179
+ if (dragPreviewOptions) {
1180
+ nodeDragProps.dragPreviewOptions = dragPreviewOptions;
1181
+ }
1182
+
1183
+ // Add drag preview rendering
1184
+ nodeDragProps.getDragProxy = getCustomDragProxy ?
1185
+ (dragItem) => getCustomDragProxy(item, getSelection()) :
1186
+ null; // Let GlobalDragProxy handle the default case
1187
+ }
1188
+ if (areNodesDropTarget) {
1189
+ WhichNode = DropTargetTreeNode;
1190
+ nodeDragProps.isDropTarget = true;
1191
+ nodeDragProps.dropTargetAccept = dropTargetAccept;
1192
+ nodeDragProps.canDrop = (droppedItem, monitor) => {
1193
+ // Check if the drop operation would be valid based on business rules
1194
+ if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1195
+ return canNodeAcceptDrop(item, droppedItem);
1196
+ }
1197
+ // Default: allow external drops
1198
+ return true;
1199
+ };
1200
+ nodeDragProps.onDrop = (droppedItem) => {
1201
+ // NOTE: item is sometimes getting destroyed, but it still has the id, so you can still use it
1202
+ onNodeDrop(item, droppedItem);
1203
+ };
1204
+ }
1205
+ if (areNodesDragSource && areNodesDropTarget) {
1206
+ WhichNode = DragSourceDropTargetTreeNode;
1207
+ }
1208
+ }
1209
+ }
1059
1210
  }
1060
1211
 
1061
- return <WhichTreeNode
1212
+ return <WhichNode
1062
1213
  datum={datum}
1063
1214
  nodeProps={nodeProps}
1064
1215
  onToggle={onToggle}
1216
+ isNodeSelectable={isNodeSelectable}
1217
+ isNodeHoverable={isNodeHoverable}
1065
1218
  isSelected={isSelected}
1066
1219
  isHovered={hovered}
1067
- isDragMode={isDragMode}
1220
+ showHovers={showHovers}
1221
+ showSelectHandle={showSelectHandle}
1068
1222
  isHighlighted={highlitedDatum === datum}
1069
- {...dragProps}
1223
+ {...nodeDragProps}
1070
1224
 
1071
1225
  // fields={fields}
1072
1226
  />;
@@ -1088,154 +1242,6 @@ function TreeComponent(props) {
1088
1242
  }
1089
1243
  });
1090
1244
  return nodes;
1091
- },
1092
-
1093
- // drag/drop
1094
- getDragProxy = (node) => {
1095
-
1096
- // TODO: Maybe the proxy should grab itself and all descendants??
1097
-
1098
- const
1099
- row = node,
1100
- rowRect = row.getBoundingClientRect(),
1101
- parent = row.parentElement,
1102
- parentRect = parent.getBoundingClientRect(),
1103
- proxy = row.cloneNode(true),
1104
- top = rowRect.top - parentRect.top,
1105
- rows = _.filter(parent.children, (childNode) => {
1106
- if (childNode.getBoundingClientRect().height === 0 && childNode.style.visibility !== 'hidden') {
1107
- return false; // Skip zero-height children
1108
- }
1109
- if (childNode === proxy) {
1110
- return false;
1111
- }
1112
- return true;
1113
- }),
1114
- dragRowIx = Array.from(rows).indexOf(row),
1115
- dragRowRecord = rowToDatumMap[dragRowIx].item;
1116
-
1117
- setDragNodeId(dragRowRecord.id); // the id of which record is being dragged
1118
-
1119
- proxy.style.top = top + 'px';
1120
- proxy.style.left = (dragRowRecord.depth * DEPTH_INDENT_PX) + 'px';
1121
- proxy.style.height = rowRect.height + 'px';
1122
- proxy.style.width = rowRect.width + 'px';
1123
- proxy.style.display = 'flex';
1124
- proxy.style.position = 'absolute';
1125
- proxy.style.border = '1px solid #bbb';
1126
- return proxy;
1127
- },
1128
- onDrag = (info, e, proxy, node) => {
1129
- // console.log('onDrag', info, e, proxy, node);
1130
- const
1131
- proxyRect = proxy.getBoundingClientRect(),
1132
- row = node,
1133
- parent = row.parentElement,
1134
- parentRect = parent.getBoundingClientRect(),
1135
- rows = _.filter(parent.children, (childNode) => {
1136
- if (childNode.getBoundingClientRect().height === 0 && childNode.style.visibility !== 'hidden') {
1137
- return false; // Skip zero-height children
1138
- }
1139
- if (childNode === proxy) {
1140
- return false;
1141
- }
1142
- return true;
1143
- }),
1144
- currentY = proxyRect.top - parentRect.top; // top position of pointer, relative to page
1145
-
1146
- // Figure out which row the user wants as a parentId
1147
- let newIx = 0; // default to root being new parentId
1148
- _.each(rows, (child, ix, all) => {
1149
- const
1150
- rect = child.getBoundingClientRect(), // rect of the row of this iteration
1151
- {
1152
- top,
1153
- bottom,
1154
- height,
1155
- } = rect,
1156
- compensatedBottom = bottom - parentRect.top;
1157
-
1158
- if (child === proxy) {
1159
- return;
1160
- }
1161
- if (ix === 0) {
1162
- // first row
1163
- if (currentY < compensatedBottom) {
1164
- newIx = 0;
1165
- return false;
1166
- }
1167
- return;
1168
- } else if (ix === all.length -1) {
1169
- // last row
1170
- if (currentY < compensatedBottom) {
1171
- newIx = ix;
1172
- return false;
1173
- }
1174
- return;
1175
- }
1176
-
1177
- // all other rows
1178
- if (currentY < compensatedBottom) {
1179
- newIx = ix;
1180
- return false;
1181
- }
1182
- });
1183
-
1184
-
1185
- const
1186
- dragDatum = getDatumById(dragNodeId),
1187
- dragDatumChildIds = getDatumChildIds(dragDatum),
1188
- dropRowDatum = rowToDatumMap[newIx],
1189
- dropRowRecord = dropRowDatum.item,
1190
- dropNodeId = dropRowRecord.id,
1191
- dragNodeContainsDropNode = inArray(dropNodeId, dragDatumChildIds) || dropRowRecord.id === dragNodeId;
1192
-
1193
- if (dragNodeContainsDropNode) {
1194
- // the node can be a child of any node except itself or its own descendants
1195
- setDropRowIx(null);
1196
- setHighlitedDatum(null);
1197
-
1198
- } else {
1199
- // console.log('setDropRowIx', newIx);
1200
- setDropRowIx(newIx);
1201
-
1202
- // highlight the drop node
1203
- setHighlitedDatum(dropRowDatum);
1204
-
1205
- // shift proxy's depth
1206
- const depth = (dropRowRecord.id === dragNodeId) ? dropRowRecord.depth : dropRowRecord.depth + 1;
1207
- proxy.style.left = (depth * DEPTH_INDENT_PX) + 'px';
1208
- }
1209
- },
1210
- onDragStop = async (delta, e, config) => {
1211
- // console.log('onDragStop', delta, e, config);
1212
-
1213
- if (_.isNil(dropRowIx)) {
1214
- return;
1215
- }
1216
-
1217
- const
1218
- dragDatum = getDatumById(dragNodeId),
1219
- dragRowRecord = dragDatum.item,
1220
- dropRowDatum = rowToDatumMap[dropRowIx],
1221
- dropRowRecord = dropRowDatum.item;
1222
-
1223
- if (Repository) {
1224
- if (!Repository.isDestroyed) {
1225
- const commonAncestorId = await Repository.moveTreeNode(dragRowRecord, dropRowRecord.id);
1226
- const commonAncestorDatum = getDatumById(commonAncestorId);
1227
- reloadNode(commonAncestorDatum.item);
1228
- }
1229
- } else {
1230
-
1231
- throw Error('Not yet implemented');
1232
- // function arrayMove(arr, fromIndex, toIndex) {
1233
- // var element = arr[fromIndex];
1234
- // arr.splice(fromIndex, 1);
1235
- // arr.splice(toIndex, 0, element);
1236
- // }
1237
- // arrayMove(data, dragNodeIx, finalDropIx);
1238
- }
1239
1245
  };
1240
1246
 
1241
1247
  useEffect(() => {
@@ -1331,8 +1337,8 @@ function TreeComponent(props) {
1331
1337
  }
1332
1338
 
1333
1339
  const
1334
- headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue, isDragMode, getTreeNodeData()]),
1335
- footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, isDragMode, getTreeNodeData()]);
1340
+ headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue, getTreeNodeData()]),
1341
+ footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, getTreeNodeData()]);
1336
1342
 
1337
1343
  if (!isReady) {
1338
1344
  return <CenterBox>
@@ -1365,18 +1371,10 @@ function TreeComponent(props) {
1365
1371
  w-full
1366
1372
  min-w-[300px]
1367
1373
  `;
1368
- if (isDragMode) {
1369
- className += `
1370
- ${styles.GRID_REORDER_BORDER_COLOR}
1371
- ${styles.GRID_REORDER_BORDER_WIDTH}
1372
- ${styles.GRID_REORDER_BORDER_STYLE}
1373
- `;
1374
+ if (isLoading) {
1375
+ className += ' border-t-2 border-[#f00]';
1374
1376
  } else {
1375
- if (isLoading) {
1376
- className += ' border-t-2 border-[#f00]';
1377
- } else {
1378
- className += ' border-t-1 border-grey-300';
1379
- }
1377
+ className += ' border-t-1 border-grey-300';
1380
1378
  }
1381
1379
  if (props.className) {
1382
1380
  className += ' ' + props.className;
@@ -1394,9 +1392,7 @@ function TreeComponent(props) {
1394
1392
  <VStack
1395
1393
  ref={treeRef}
1396
1394
  onClick={() => {
1397
- if (!isDragMode) {
1398
- deselectAll();
1399
- }
1395
+ deselectAll();
1400
1396
  }}
1401
1397
  className="Tree-deselector w-full flex-1 p-1 bg-white"
1402
1398
  >
@@ -1426,15 +1422,17 @@ export const Tree = withComponent(
1426
1422
  withEvents(
1427
1423
  withData(
1428
1424
  withPermissions(
1429
- // withMultiSelection(
1430
- withSelection(
1431
- withFilters(
1432
- withContextMenu(
1433
- TreeComponent
1425
+ withDropTarget(
1426
+ // withMultiSelection(
1427
+ withSelection(
1428
+ withFilters(
1429
+ withContextMenu(
1430
+ TreeComponent
1431
+ )
1434
1432
  )
1435
1433
  )
1436
- )
1437
- // )
1434
+ // )
1435
+ )
1438
1436
  )
1439
1437
  )
1440
1438
  )
@@ -1446,20 +1444,22 @@ export const SideTreeEditor = withComponent(
1446
1444
  withEvents(
1447
1445
  withData(
1448
1446
  withPermissions(
1449
- // withMultiSelection(
1450
- withSelection(
1451
- withSideEditor(
1452
- withFilters(
1453
- withPresetButtons(
1454
- withContextMenu(
1455
- TreeComponent
1447
+ withDropTarget(
1448
+ // withMultiSelection(
1449
+ withSelection(
1450
+ withSideEditor(
1451
+ withFilters(
1452
+ withPresetButtons(
1453
+ withContextMenu(
1454
+ TreeComponent
1455
+ )
1456
1456
  )
1457
- )
1458
- ),
1459
- true // isTree
1457
+ ),
1458
+ true // isTree
1459
+ )
1460
1460
  )
1461
- )
1462
- // )
1461
+ // )
1462
+ )
1463
1463
  )
1464
1464
  )
1465
1465
  )
@@ -1471,20 +1471,22 @@ export const WindowedTreeEditor = withComponent(
1471
1471
  withEvents(
1472
1472
  withData(
1473
1473
  withPermissions(
1474
- // withMultiSelection(
1475
- withSelection(
1476
- withWindowedEditor(
1477
- withFilters(
1478
- withPresetButtons(
1479
- withContextMenu(
1480
- TreeComponent
1474
+ withDropTarget(
1475
+ // withMultiSelection(
1476
+ withSelection(
1477
+ withWindowedEditor(
1478
+ withFilters(
1479
+ withPresetButtons(
1480
+ withContextMenu(
1481
+ TreeComponent
1482
+ )
1481
1483
  )
1482
- )
1483
- ),
1484
- true // isTree
1484
+ ),
1485
+ true // isTree
1486
+ )
1485
1487
  )
1486
- )
1487
- // )
1488
+ // )
1489
+ )
1488
1490
  )
1489
1491
  )
1490
1492
  )