@onehat/ui 0.4.65 → 0.4.67

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,19 +47,15 @@ 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';
48
54
  import Dot from '../Icons/Dot.js';
49
55
  import Collapse from '../Icons/Collapse.js';
50
56
  import Expand from '../Icons/Expand.js';
51
- import FolderClosed from '../Icons/FolderClosed.js';
52
- import FolderOpen from '../Icons/FolderOpen.js';
53
57
  import Gear from '../Icons/Gear.js';
54
58
  import MagnifyingGlass from '../Icons/MagnifyingGlass.js';
55
- import NoReorderRows from '../Icons/NoReorderRows.js';
56
- import ReorderRows from '../Icons/ReorderRows.js';
57
59
  import PaginationToolbar from '../Toolbar/PaginationToolbar.js';
58
60
  import NoRecordsFound from '../Grid/NoRecordsFound.js';
59
61
  import Toolbar from '../Toolbar/Toolbar.js';
@@ -68,6 +70,8 @@ const
68
70
  DOUBLE_CLICK = 2,
69
71
  TRIPLE_CLICK = 3;
70
72
 
73
+ // NOTE: If using TreeComponent with getCustomDragProxy, ensure that <GlobalDragProxy /> exists in App.js
74
+
71
75
  function TreeComponent(props) {
72
76
  const {
73
77
  areRootsVisible = true,
@@ -87,21 +91,9 @@ function TreeComponent(props) {
87
91
  getDisplayTextFromSearchResults = (item) => {
88
92
  return item.id
89
93
  },
90
- getNodeIcon = (which, item) => { // decides what icon to show for this node
94
+ getNodeIcon = (item) => {
91
95
  // TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
92
- let icon;
93
- switch(which) {
94
- case COLLAPSED:
95
- icon = FolderClosed;
96
- break;
97
- case EXPANDED:
98
- icon = FolderOpen;
99
- break;
100
- case LEAF:
101
- icon = Dot;
102
- break;
103
- }
104
- return icon;
96
+ return Dot;
105
97
  },
106
98
  getNodeProps = (item) => {
107
99
  return {};
@@ -110,7 +102,9 @@ function TreeComponent(props) {
110
102
  disableLoadingIndicator = false,
111
103
  disableSelectorSelected = false,
112
104
  showHovers = true,
113
- canNodesReorder = false,
105
+ showSelectHandle = true,
106
+ isNodeSelectable = true,
107
+ isNodeHoverable = true,
114
108
  allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the node clicked on
115
109
  disableBottomToolbar = false,
116
110
  bottomToolbar = null,
@@ -122,11 +116,24 @@ function TreeComponent(props) {
122
116
  canRecordBeEdited,
123
117
  onTreeLoad,
124
118
  onLayout,
125
-
126
119
  selectorId,
127
120
  selectorSelected,
128
121
  selectorSelectedField = 'id',
129
122
 
123
+ // DND
124
+ canNodesMoveInternally = false,
125
+ canNodeMoveInternally, // optional fn to customize whether each node can be dragged INternally
126
+ canNodeMoveExternally, // optional fn to customize whether each node can be dragged EXternally
127
+ canNodeAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
128
+ getCustomDragProxy, // optional fn to render custom drag preview: (item, selection) => ReactElement
129
+ dragPreviewOptions, // optional object for drag preview positioning options
130
+ areNodesDragSource = false,
131
+ nodeDragSourceType,
132
+ getNodeDragSourceItem,
133
+ areNodesDropTarget = false,
134
+ dropTargetAccept,
135
+ onNodeDrop,
136
+
130
137
  // withComponent
131
138
  self,
132
139
 
@@ -161,6 +168,12 @@ function TreeComponent(props) {
161
168
  // withPermissions
162
169
  canUser,
163
170
 
171
+ // withDnd
172
+ isDropTarget,
173
+ canDrop,
174
+ isOver,
175
+ dropTargetRef,
176
+
164
177
  // withSelection
165
178
  selection,
166
179
  setSelection,
@@ -173,19 +186,15 @@ function TreeComponent(props) {
173
186
  noSelectorMeansNoResults = false,
174
187
 
175
188
  } = props,
176
- styles = UiGlobals.styles,
177
189
  forceUpdate = useForceUpdate(),
178
190
  treeRef = useRef(),
179
191
  treeNodeData = useRef(),
192
+ dragSelectionRef = useRef([]),
180
193
  [isReady, setIsReady] = useState(false),
181
194
  [isLoading, setIsLoading] = useState(false),
182
- [rowToDatumMap, setRowToDatumMap] = useState({}),
183
195
  [searchResults, setSearchResults] = useState([]),
184
196
  [searchFormData, setSearchFormData] = useState([]),
185
197
  [highlitedDatum, setHighlitedDatum] = useState(null),
186
- [isDragMode, setIsDragMode] = useState(false),
187
- [dragNodeId, setDragNodeId] = useState(null),
188
- [dropRowIx, setDropRowIx] = useState(null),
189
198
  [treeSearchValue, setTreeSearchValue] = useState(''),
190
199
 
191
200
  // state getters & setters
@@ -270,7 +279,7 @@ function TreeComponent(props) {
270
279
  }
271
280
  const
272
281
  parent = selection[0],
273
- parentDatum = getNodeData(parent.id);
282
+ parentDatum = getDatumById(parent.id);
274
283
 
275
284
  if (parent.hasChildren && !parent.areChildrenLoaded) {
276
285
  await loadChildren(parentDatum);
@@ -284,7 +293,7 @@ function TreeComponent(props) {
284
293
  // Add the entity to the tree, show parent as hasChildren and expanded
285
294
  const
286
295
  parent = selection[0],
287
- parentDatum = getNodeData(parent.id);
296
+ parentDatum = getDatumById(parent.id);
288
297
  if (!parent.hasChildren) {
289
298
  parent.hasChildren = true; // since we're adding a new child
290
299
  }
@@ -292,7 +301,6 @@ function TreeComponent(props) {
292
301
  parentDatum.isExpanded = true;
293
302
  }
294
303
 
295
- buildRowToDatumMap();
296
304
  forceUpdate();
297
305
  },
298
306
  onAfterAddSave = (entities) => {
@@ -307,7 +315,7 @@ function TreeComponent(props) {
307
315
  // Refresh the node's display
308
316
  const
309
317
  node = entities[0],
310
- existingDatum = getNodeData(node.id), // TODO: Make this work for >1 entity
318
+ existingDatum = getDatumById(node.id), // TODO: Make this work for >1 entity
311
319
  newDatum = buildTreeNodeDatum(node);
312
320
 
313
321
  // copy the updated data to existingDatum
@@ -317,7 +325,7 @@ function TreeComponent(props) {
317
325
 
318
326
  if (node.parent?.id) {
319
327
  const
320
- existingParentDatum = getNodeData(node.parent.id),
328
+ existingParentDatum = getDatumById(node.parent.id),
321
329
  newParentDatum = buildTreeNodeDatum(node.parent);
322
330
  _.assign(existingParentDatum, newParentDatum);
323
331
  existingParentDatum.isExpanded = true;
@@ -331,7 +339,7 @@ function TreeComponent(props) {
331
339
  onBeforeSave = (entities) => {
332
340
  const
333
341
  node = entities[0],
334
- datum = getNodeData(node.id); // TODO: Make this work for >1 entity
342
+ datum = getDatumById(node.id); // TODO: Make this work for >1 entity
335
343
 
336
344
  datum.isLoading = true;
337
345
  forceUpdate();
@@ -339,9 +347,7 @@ function TreeComponent(props) {
339
347
  onAfterDelete = async (entities) => {
340
348
  const parent = entities[0].parent;
341
349
  if (parent) {
342
- await reloadNode(parent); // includes buildRowToDatumMap
343
- } else {
344
- buildRowToDatumMap();
350
+ await reloadNode(parent);
345
351
  }
346
352
  },
347
353
  onToggle = async (datum, e) => {
@@ -376,7 +382,6 @@ function TreeComponent(props) {
376
382
  }
377
383
 
378
384
  forceUpdate();
379
- buildRowToDatumMap();
380
385
  },
381
386
  onCollapseAll = () => {
382
387
  const newTreeNodeData = _.clone(getTreeNodeData());
@@ -473,35 +478,50 @@ function TreeComponent(props) {
473
478
  });
474
479
  },
475
480
 
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;
481
+ // internal DND
482
+ onInternalNodeDrop = async (droppedOn, droppedItem) => {
483
+ let selectedNodes = [];
484
+ if (droppedItem.getSelection) {
485
+ selectedNodes = droppedItem.getSelection();
494
486
  }
495
- let found = null;
496
- _.each(getTreeNodeData(), (node) => {
497
- const foundNode = findNodeById(node);
498
- if (foundNode) {
499
- found = foundNode;
500
- return false;
501
- }
487
+ if (_.isEmpty(selectedNodes)) {
488
+ selectedNodes = [droppedItem.item];
489
+ }
490
+
491
+ // filter out nodes that would already be moved by others in the selection
492
+ const selectedNodesClone = [...selectedNodes];
493
+ selectedNodes = selectedNodes.filter((node) => {
494
+ let isDescendant = false;
495
+ _.each(selectedNodesClone, (otherNode) => {
496
+ if (node.id === otherNode.id) {
497
+ return false; // skip self
498
+ }
499
+ isDescendant = isDescendantOf(node, otherNode);
500
+ if (isDescendant) {
501
+ return false; // found descendant; break loop
502
+ }
503
+ isDescendant = isDescendantOf(otherNode, node);
504
+ if (isDescendant) {
505
+ return false; // found ancestor; break loop
506
+ }
507
+ });
508
+ return !isDescendant;
502
509
  });
503
- return found;
510
+
511
+ const isMultiSelection = selectedNodes.length > 1;
512
+ if (isMultiSelection) {
513
+ alert('moving multiple disparate nodes not yet implemented');
514
+ return;
515
+ }
516
+
517
+ const selectedNode = selectedNodes[0];
518
+ const commonAncestorId = await Repository.moveTreeNode(selectedNode, droppedOn.id);
519
+ const commonAncestorDatum = getDatumById(commonAncestorId);
520
+ reloadNode(commonAncestorDatum.item);
521
+
504
522
  },
523
+
524
+ // utilities
505
525
  buildTreeNodeDatum = (treeNode, defaultToExpanded = false) => {
506
526
  // Build the data-representation of one node and its children,
507
527
  // caching text & icon, keeping track of the state for whole tree
@@ -511,11 +531,10 @@ function TreeComponent(props) {
511
531
  children = buildTreeNodeData(treeNode.children, defaultToExpanded), // recursively get data for children
512
532
  datum = {
513
533
  item: treeNode,
534
+ treeRef,
514
535
  text: getNodeText(treeNode),
515
536
  content: getNodeContent ? getNodeContent(treeNode) : null,
516
- iconCollapsed: getNodeIcon(COLLAPSED, treeNode),
517
- iconExpanded: getNodeIcon(EXPANDED, treeNode),
518
- iconLeaf: getNodeIcon(LEAF, treeNode),
537
+ icon: getNodeIcon(treeNode),
519
538
  isExpanded: treeNode.isExpanded || defaultToExpanded || isRoot, // all non-root treeNodes are collapsed by default
520
539
  isVisible: isRoot ? areRootsVisible : true,
521
540
  isLoading: false,
@@ -548,37 +567,39 @@ function TreeComponent(props) {
548
567
  const treeNodeData = buildTreeNodeData(nodes);
549
568
  setTreeNodeData(treeNodeData);
550
569
 
551
- buildRowToDatumMap();
552
-
553
570
  if (onTreeLoad) {
554
571
  onTreeLoad(self);
555
572
  }
556
573
  return treeNodeData;
557
574
  },
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++;
575
+ buildAndSetOneTreeNodeData = (entity) => {
576
+
577
+ if (!entity || !entity.parent) {
578
+ // If no parent, it might be a root node, so rebuild the tree
579
+ buildAndSetTreeNodeData();
580
+ return;
581
+ }
570
582
 
571
- if (datum.isExpanded) {
572
- _.each(datum.children, (child) => {
573
- walkTree(child);
574
- });
575
- }
583
+ const parentDatum = getDatumById(entity.parent.id);
584
+ if (!parentDatum) {
585
+ // Parent not found in current tree structure, rebuild
586
+ buildAndSetTreeNodeData();
587
+ return;
576
588
  }
577
- _.each(getTreeNodeData(), (rootDatum) => {
578
- walkTree(rootDatum);
579
- });
589
+
590
+ // Create datum for the new entity and add it to parent's children
591
+ const newDatum = buildTreeNodeDatum(entity);
592
+ parentDatum.children.push(newDatum);
580
593
 
581
- setRowToDatumMap(rowToDatumMap);
594
+ // Update parent to show it has children and expand if needed
595
+ if (!entity.parent.hasChildren) {
596
+ entity.parent.hasChildren = true;
597
+ }
598
+ if (!parentDatum.isExpanded) {
599
+ parentDatum.isExpanded = true;
600
+ }
601
+
602
+ forceUpdate();
582
603
  },
583
604
  datumContainsSelection = (datum) => {
584
605
  if (_.isEmpty(selection)) {
@@ -698,13 +719,38 @@ function TreeComponent(props) {
698
719
 
699
720
  return treeNodes;
700
721
  },
722
+ belongsToThisTree = (treeNode) => {
723
+ if (!treeNode) {
724
+ return false;
725
+ }
726
+ const datum = getDatumById(treeNode.id);
727
+ if (!datum) {
728
+ return false;
729
+ }
730
+ return datum.treeRef === treeRef;
731
+ },
732
+ isDescendantOf = (potentialDescendant, potentialAncestor) => {
733
+ // Check if potentialDescendant is a descendant of potentialAncestor
734
+ // by walking up the parent chain from potentialDescendant
735
+ let currentTreeNode = potentialDescendant;
736
+ while(currentTreeNode) {
737
+ if (currentTreeNode.id === potentialAncestor.id) {
738
+ return true;
739
+ }
740
+ currentTreeNode = currentTreeNode.parent;
741
+ }
742
+ return false;
743
+ },
744
+ isChildOf = (potentialChild, potentialParent) => {
745
+ return potentialChild.parent?.id === potentialParent.id;
746
+ },
701
747
  reloadTree = () => {
702
748
  Repository.areRootNodesLoaded = false;
703
749
  return buildAndSetTreeNodeData();
704
750
  },
705
751
  reloadNode = async (node) => {
706
752
  // mark node as loading
707
- const existingDatum = getNodeData(node.id);
753
+ const existingDatum = getDatumById(node.id);
708
754
  existingDatum.isLoading = true;
709
755
  forceUpdate();
710
756
 
@@ -718,8 +764,6 @@ function TreeComponent(props) {
718
764
  _.assign(existingDatum, _.omit(newDatum, ['isExpanded']));
719
765
  existingDatum.isLoading = false;
720
766
  forceUpdate();
721
-
722
- buildRowToDatumMap();
723
767
  },
724
768
  loadChildren = async (datum, depth = 1) => {
725
769
 
@@ -755,12 +799,9 @@ function TreeComponent(props) {
755
799
  // Hide loading indicator
756
800
  datum.isLoading = false;
757
801
  forceUpdate();
758
-
759
- buildRowToDatumMap();
760
802
  },
761
803
  collapseNodes = (nodes) => {
762
804
  collapseNodesRecursive(nodes);
763
- buildRowToDatumMap();
764
805
  },
765
806
  collapseNodesRecursive = (nodes) => {
766
807
  _.each(nodes, (node) => {
@@ -779,7 +820,6 @@ function TreeComponent(props) {
779
820
 
780
821
  // expand them in UI
781
822
  expandNodesRecursive(nodes);
782
- buildRowToDatumMap();
783
823
  },
784
824
  expandNodesRecursive = (nodes) => {
785
825
  _.each(nodes, (node) => {
@@ -846,7 +886,6 @@ function TreeComponent(props) {
846
886
  }
847
887
 
848
888
  setTreeNodeData(newTreeNodeData);
849
- buildRowToDatumMap();
850
889
  },
851
890
  scrollToNode = (node) => {
852
891
  // Helper for expandPath
@@ -907,17 +946,6 @@ function TreeComponent(props) {
907
946
  isDisabled: false,
908
947
  },
909
948
  ];
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
949
  if (isNodeTextConfigurable && editDisplaySettings) {
922
950
  buttons.push({
923
951
  key: 'editNodeTextBtn',
@@ -978,9 +1006,6 @@ function TreeComponent(props) {
978
1006
  if (e.preventDefault && e.cancelable) {
979
1007
  e.preventDefault();
980
1008
  }
981
- if (isDragMode) {
982
- return
983
- }
984
1009
  switch (e.detail) {
985
1010
  case SIMULATED_CLICK:
986
1011
  case SINGLE_CLICK:
@@ -1014,9 +1039,6 @@ function TreeComponent(props) {
1014
1039
  if (e.preventDefault && e.cancelable) {
1015
1040
  e.preventDefault();
1016
1041
  }
1017
- if (isDragMode) {
1018
- return;
1019
- }
1020
1042
 
1021
1043
  if (!setSelection) {
1022
1044
  return;
@@ -1030,6 +1052,8 @@ function TreeComponent(props) {
1030
1052
  }
1031
1053
  }}
1032
1054
  className={`
1055
+ Pressable
1056
+ Node
1033
1057
  flex-row
1034
1058
  `}
1035
1059
  style={{
@@ -1041,32 +1065,197 @@ function TreeComponent(props) {
1041
1065
  focused,
1042
1066
  pressed,
1043
1067
  }) => {
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';
1068
+ const nodeDragProps = {};
1069
+ let WhichNode = TreeNode;
1070
+ if (CURRENT_MODE === UI_MODE_WEB) { // DND is currently web-only TODO: implement for RN
1071
+ // Create a method that gets an always-current copy of the selection ids
1072
+ dragSelectionRef.current = selection;
1073
+ const getSelection = () => dragSelectionRef.current;
1074
+
1075
+ const userHasPermissionToDrag = (!canUser || canUser(EDIT));
1076
+ if (userHasPermissionToDrag) {
1077
+ // NOTE: The Tree can either drag nodes internally or externally, but not both at the same time!
1078
+
1079
+ // assign event handlers
1080
+ if (canNodesMoveInternally) {
1081
+ // internal drag/drop
1082
+ const nodeDragSourceType = 'internal';
1083
+ WhichNode = DragSourceDropTargetTreeNode;
1084
+ nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
1085
+ nodeDragProps.dragSourceType = nodeDragSourceType;
1086
+ nodeDragProps.dragSourceItem = {
1087
+ id: item.id,
1088
+ item,
1089
+ getSelection,
1090
+ type: nodeDragSourceType,
1091
+ };
1092
+
1093
+ // Prevent root nodes from being dragged, and use custom logic if provided
1094
+ nodeDragProps.canDrag = (monitor) => {
1095
+ const currentSelection = getSelection();
1096
+
1097
+ // Check if any selected node is a root node (can't drag root nodes)
1098
+ const hasRootNode = currentSelection.some(node => node.isRoot);
1099
+ if (hasRootNode) {
1100
+ return false;
1101
+ }
1102
+
1103
+ // Use custom drag validation if provided
1104
+ if (canNodeMoveInternally) {
1105
+ // In multi-selection, all nodes must be draggable
1106
+ return currentSelection.every(node => canNodeMoveInternally(node));
1107
+ }
1108
+
1109
+ return true;
1110
+ };
1111
+
1112
+ // Add custom drag preview options
1113
+ if (dragPreviewOptions) {
1114
+ nodeDragProps.dragPreviewOptions = dragPreviewOptions;
1115
+ }
1116
+
1117
+ // Add drag preview rendering
1118
+ nodeDragProps.getDragProxy = getCustomDragProxy ?
1119
+ (dragItem) => getCustomDragProxy(item, getSelection()) :
1120
+ null; // Let GlobalDragProxy handle the default case
1121
+
1122
+ const dropTargetAccept = 'internal';
1123
+ nodeDragProps.isDropTarget = true;
1124
+ nodeDragProps.dropTargetAccept = dropTargetAccept;
1125
+
1126
+ // Define validation logic once for reuse
1127
+ const validateDrop = (draggedItem) => {
1128
+ if (!draggedItem) {
1129
+ return false;
1130
+ }
1131
+
1132
+ const currentSelection = getSelection();
1133
+
1134
+ // Always include the dragged item itself in validation
1135
+ // If no selection exists, the dragged item is what we're moving
1136
+ const nodesToValidate = currentSelection.length > 0 ? currentSelection : [draggedItem.item];
1137
+
1138
+ // validate that the dropped item is not already a direct child of the target node
1139
+ if (isChildOf(draggedItem.item, item)) {
1140
+ return false;
1141
+ }
1142
+
1143
+ // Validate that none of the nodes being moved can be dropped into the target location
1144
+ for (const nodeToMove of nodesToValidate) {
1145
+ if (nodeToMove.id === item.id) {
1146
+ // Cannot drop a node onto itself
1147
+ return false;
1148
+ }
1149
+ if (isDescendantOf(item, nodeToMove)) {
1150
+ // Cannot drop a node into its own descendants
1151
+ return false;
1152
+ }
1153
+ }
1154
+
1155
+ if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1156
+ // custom business logic
1157
+ return canNodeAcceptDrop(item, draggedItem);
1158
+ }
1159
+ return true;
1160
+ };
1161
+
1162
+ // Use the validation function for React DnD
1163
+ nodeDragProps.canDrop = (draggedItem, monitor) => validateDrop(draggedItem);
1164
+
1165
+ // Pass the same validation function for visual feedback
1166
+ nodeDragProps.validateDrop = validateDrop;
1167
+
1168
+ nodeDragProps.onDrop = (droppedItem) => {
1169
+ if (belongsToThisTree(droppedItem)) {
1170
+ onInternalNodeDrop(item, droppedItem);
1171
+ }
1172
+ };
1173
+ } else {
1174
+ // external drag/drop
1175
+ if (areNodesDragSource) {
1176
+ WhichNode = DragSourceTreeNode;
1177
+ nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
1178
+ nodeDragProps.dragSourceType = nodeDragSourceType;
1179
+ if (getNodeDragSourceItem) {
1180
+ nodeDragProps.dragSourceItem = getNodeDragSourceItem(item, getSelection, nodeDragSourceType);
1181
+ } else {
1182
+ nodeDragProps.dragSourceItem = {
1183
+ id: item.id,
1184
+ item,
1185
+ getSelection,
1186
+ type: nodeDragSourceType,
1187
+ };
1188
+ }
1189
+ if (canNodeMoveExternally) {
1190
+ nodeDragProps.canDrag = canNodeMoveExternally;
1191
+ }
1192
+
1193
+ // Add custom drag preview options
1194
+ if (dragPreviewOptions) {
1195
+ nodeDragProps.dragPreviewOptions = dragPreviewOptions;
1196
+ }
1197
+
1198
+ // Add drag preview rendering
1199
+ nodeDragProps.getDragProxy = getCustomDragProxy ?
1200
+ (dragItem) => getCustomDragProxy(item, getSelection()) :
1201
+ null; // Let GlobalDragProxy handle the default case
1202
+ }
1203
+ if (areNodesDropTarget) {
1204
+ WhichNode = DropTargetTreeNode;
1205
+ nodeDragProps.isDropTarget = true;
1206
+ nodeDragProps.dropTargetAccept = dropTargetAccept;
1207
+ nodeDragProps.canDrop = (droppedItem, monitor) => {
1208
+ // Check if the drop operation would be valid based on business rules
1209
+ if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1210
+ return canNodeAcceptDrop(item, droppedItem);
1211
+ }
1212
+ // Default: allow external drops
1213
+ return true;
1214
+ };
1215
+
1216
+ // Define validation logic once for reuse
1217
+ const validateDrop = (draggedItem) => {
1218
+ if (!draggedItem) {
1219
+ return false;
1220
+ }
1221
+
1222
+ if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1223
+ // custom business logic
1224
+ return canNodeAcceptDrop(item, draggedItem);
1225
+ }
1226
+ return true;
1227
+ };
1228
+
1229
+ // Use the validation function for React DnD
1230
+ nodeDragProps.canDrop = (draggedItem, monitor) => validateDrop(draggedItem);
1231
+
1232
+ // Pass the same validation function for visual feedback
1233
+ nodeDragProps.validateDrop = validateDrop;
1234
+
1235
+ nodeDragProps.onDrop = (droppedItem) => {
1236
+ // NOTE: item is sometimes getting destroyed, but it still has the id, so you can still use it
1237
+ onNodeDrop(item, droppedItem);
1238
+ };
1239
+ }
1240
+ if (areNodesDragSource && areNodesDropTarget) {
1241
+ WhichNode = DragSourceDropTargetTreeNode;
1242
+ }
1243
+ }
1244
+ }
1059
1245
  }
1060
1246
 
1061
- return <WhichTreeNode
1247
+ return <WhichNode
1062
1248
  datum={datum}
1063
1249
  nodeProps={nodeProps}
1064
1250
  onToggle={onToggle}
1251
+ isNodeSelectable={isNodeSelectable}
1252
+ isNodeHoverable={isNodeHoverable}
1065
1253
  isSelected={isSelected}
1066
1254
  isHovered={hovered}
1067
- isDragMode={isDragMode}
1255
+ showHovers={showHovers}
1256
+ showSelectHandle={showSelectHandle}
1068
1257
  isHighlighted={highlitedDatum === datum}
1069
- {...dragProps}
1258
+ {...nodeDragProps}
1070
1259
 
1071
1260
  // fields={fields}
1072
1261
  />;
@@ -1088,154 +1277,6 @@ function TreeComponent(props) {
1088
1277
  }
1089
1278
  });
1090
1279
  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
1280
  };
1240
1281
 
1241
1282
  useEffect(() => {
@@ -1268,7 +1309,7 @@ function TreeComponent(props) {
1268
1309
  Repository.on('load', setFalse);
1269
1310
  Repository.on('loadRootNodes', setFalse);
1270
1311
  Repository.on('loadRootNodes', buildAndSetTreeNodeData);
1271
- Repository.on('add', buildAndSetTreeNodeData);
1312
+ Repository.on('add', buildAndSetOneTreeNodeData);
1272
1313
  Repository.on('changeFilters', reloadTree);
1273
1314
  Repository.on('changeSorters', reloadTree);
1274
1315
 
@@ -1284,7 +1325,7 @@ function TreeComponent(props) {
1284
1325
  Repository.off('load', setFalse);
1285
1326
  Repository.off('loadRootNodes', setFalse);
1286
1327
  Repository.off('loadRootNodes', buildAndSetTreeNodeData);
1287
- Repository.off('add', buildAndSetTreeNodeData);
1328
+ Repository.off('add', buildAndSetOneTreeNodeData);
1288
1329
  Repository.off('changeFilters', reloadTree);
1289
1330
  Repository.off('changeSorters', reloadTree);
1290
1331
  };
@@ -1331,8 +1372,8 @@ function TreeComponent(props) {
1331
1372
  }
1332
1373
 
1333
1374
  const
1334
- headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue, isDragMode, getTreeNodeData()]),
1335
- footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, isDragMode, getTreeNodeData()]);
1375
+ headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue, getTreeNodeData()]),
1376
+ footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, getTreeNodeData()]);
1336
1377
 
1337
1378
  if (!isReady) {
1338
1379
  return <CenterBox>
@@ -1365,18 +1406,10 @@ function TreeComponent(props) {
1365
1406
  w-full
1366
1407
  min-w-[300px]
1367
1408
  `;
1368
- if (isDragMode) {
1369
- className += `
1370
- ${styles.GRID_REORDER_BORDER_COLOR}
1371
- ${styles.GRID_REORDER_BORDER_WIDTH}
1372
- ${styles.GRID_REORDER_BORDER_STYLE}
1373
- `;
1409
+ if (isLoading) {
1410
+ className += ' border-t-2 border-[#f00]';
1374
1411
  } else {
1375
- if (isLoading) {
1376
- className += ' border-t-2 border-[#f00]';
1377
- } else {
1378
- className += ' border-t-1 border-grey-300';
1379
- }
1412
+ className += ' border-t-1 border-grey-300';
1380
1413
  }
1381
1414
  if (props.className) {
1382
1415
  className += ' ' + props.className;
@@ -1394,9 +1427,7 @@ function TreeComponent(props) {
1394
1427
  <VStack
1395
1428
  ref={treeRef}
1396
1429
  onClick={() => {
1397
- if (!isDragMode) {
1398
- deselectAll();
1399
- }
1430
+ deselectAll();
1400
1431
  }}
1401
1432
  className="Tree-deselector w-full flex-1 p-1 bg-white"
1402
1433
  >
@@ -1426,15 +1457,17 @@ export const Tree = withComponent(
1426
1457
  withEvents(
1427
1458
  withData(
1428
1459
  withPermissions(
1429
- // withMultiSelection(
1430
- withSelection(
1431
- withFilters(
1432
- withContextMenu(
1433
- TreeComponent
1460
+ withDropTarget(
1461
+ // withMultiSelection(
1462
+ withSelection(
1463
+ withFilters(
1464
+ withContextMenu(
1465
+ TreeComponent
1466
+ )
1434
1467
  )
1435
1468
  )
1436
- )
1437
- // )
1469
+ // )
1470
+ )
1438
1471
  )
1439
1472
  )
1440
1473
  )
@@ -1446,20 +1479,22 @@ export const SideTreeEditor = withComponent(
1446
1479
  withEvents(
1447
1480
  withData(
1448
1481
  withPermissions(
1449
- // withMultiSelection(
1450
- withSelection(
1451
- withSideEditor(
1452
- withFilters(
1453
- withPresetButtons(
1454
- withContextMenu(
1455
- TreeComponent
1482
+ withDropTarget(
1483
+ // withMultiSelection(
1484
+ withSelection(
1485
+ withSideEditor(
1486
+ withFilters(
1487
+ withPresetButtons(
1488
+ withContextMenu(
1489
+ TreeComponent
1490
+ )
1456
1491
  )
1457
- )
1458
- ),
1459
- true // isTree
1492
+ ),
1493
+ true // isTree
1494
+ )
1460
1495
  )
1461
- )
1462
- // )
1496
+ // )
1497
+ )
1463
1498
  )
1464
1499
  )
1465
1500
  )
@@ -1471,20 +1506,22 @@ export const WindowedTreeEditor = withComponent(
1471
1506
  withEvents(
1472
1507
  withData(
1473
1508
  withPermissions(
1474
- // withMultiSelection(
1475
- withSelection(
1476
- withWindowedEditor(
1477
- withFilters(
1478
- withPresetButtons(
1479
- withContextMenu(
1480
- TreeComponent
1509
+ withDropTarget(
1510
+ // withMultiSelection(
1511
+ withSelection(
1512
+ withWindowedEditor(
1513
+ withFilters(
1514
+ withPresetButtons(
1515
+ withContextMenu(
1516
+ TreeComponent
1517
+ )
1481
1518
  )
1482
- )
1483
- ),
1484
- true // isTree
1519
+ ),
1520
+ true // isTree
1521
+ )
1485
1522
  )
1486
- )
1487
- // )
1523
+ // )
1524
+ )
1488
1525
  )
1489
1526
  )
1490
1527
  )