@onehat/ui 0.4.66 → 0.4.68

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.4.66",
3
+ "version": "0.4.68",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,13 +1,9 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useState } from 'react';
2
2
  import {
3
+ Box,
3
4
  Fab, FabIcon, FabLabel,
4
5
  VStack,
5
6
  } from '@project-components/Gluestack';
6
- import Animated, {
7
- useSharedValue,
8
- useAnimatedStyle,
9
- withTiming,
10
- } from 'react-native-reanimated';
11
7
  import IconButton from '../Buttons/IconButton.js';
12
8
  import FabWithTooltip from './FabWithTooltip.js';
13
9
  import EllipsisVertical from '../Icons/EllipsisVertical.js';
@@ -19,7 +15,7 @@ import Xmark from '../Icons/Xmark.js';
19
15
  export default function DynamicFab(props) {
20
16
  const {
21
17
  icon,
22
- buttons, // to show when expanded
18
+ buttons = [], // to show when expanded
23
19
  label,
24
20
  tooltip,
25
21
  tooltipPlacement = 'left',
@@ -27,13 +23,14 @@ export default function DynamicFab(props) {
27
23
  tooltipTriggerClassName,
28
24
  collapseOnPress = true,
29
25
  } = props,
30
- isExpanded = useSharedValue(0),
26
+ [isExpanded, setIsExpanded] = useState(false),
31
27
  toggleFab = useCallback(() => {
32
- isExpanded.value = isExpanded.value ? 0 : 1;
28
+ setIsExpanded(prev => !prev);
33
29
  }, []),
34
30
  buttonSpacing = 45,
35
31
  verticalOffset = 50; // to shift the entire expanded group up
36
32
 
33
+
37
34
  let className = `
38
35
  DynamicFab
39
36
  fixed
@@ -55,24 +52,19 @@ export default function DynamicFab(props) {
55
52
  onPress,
56
53
  key,
57
54
  ...btnConfigToPass
58
- } = btnConfig,
59
- animatedStyle = useAnimatedStyle(() => {
60
- return {
61
- opacity: withTiming(isExpanded.value, { duration: 200 }),
62
- pointerEvents: isExpanded.value ? 'auto' : 'none', // Disable interaction when collapsed
63
- };
64
- });
55
+ } = btnConfig;
56
+
57
+ if (!isExpanded) {
58
+ return null;
59
+ }
65
60
 
66
- return <Animated.View
61
+ return <Box
67
62
  key={ix}
68
- style={[
69
- animatedStyle,
70
- {
71
- position: 'absolute',
72
- bottom: buttonSpacing * (ix + 1) + verticalOffset, // Static vertical positioning
73
- right: 0,
74
- },
75
- ]}
63
+ style={{
64
+ position: 'absolute',
65
+ bottom: buttonSpacing * (ix + 1) + verticalOffset, // Static vertical positioning
66
+ right: 0,
67
+ }}
76
68
  >
77
69
  <IconButton
78
70
  className={`
@@ -85,12 +77,12 @@ export default function DynamicFab(props) {
85
77
  onPress={() => {
86
78
  onPress();
87
79
  if (collapseOnPress) {
88
- isExpanded.value = 0;
80
+ setIsExpanded(false);
89
81
  }
90
82
  }}
91
83
  {...btnConfigToPass}
92
84
  />
93
- </Animated.View>;
85
+ </Box>;
94
86
  })}
95
87
  <FabWithTooltip
96
88
  size="lg"
@@ -101,8 +93,8 @@ export default function DynamicFab(props) {
101
93
  tooltipClassName={tooltipClassName}
102
94
  tooltipTriggerClassName={tooltipTriggerClassName}
103
95
  >
104
- <FabIcon as={isExpanded.value ? Xmark : icon || EllipsisVertical} />
96
+ <FabIcon as={isExpanded ? Xmark : icon || EllipsisVertical} />
105
97
  {label ? <FabLabel>{label}</FabLabel> : null}
106
98
  </FabWithTooltip>
107
99
  </VStack>;
108
- };
100
+ }
@@ -162,9 +162,9 @@ function Form(props) {
162
162
  let skipAll = false;
163
163
  if (record?.isDestroyed) {
164
164
  skipAll = true; // if record is destroyed, skip render, but allow hooks to still be called
165
- if (self?.parent?.parent?.setIsEditorShown) {
166
- self.parent.parent.setIsEditorShown(false); // close the editor
167
- }
165
+ // if (self?.parent?.parent?.setIsEditorShown) {
166
+ // self.parent.parent.setIsEditorShown(false); // close the editor
167
+ // }
168
168
  }
169
169
  const
170
170
  isMultiple = _.isArray(record),
@@ -363,7 +363,7 @@ function Form(props) {
363
363
  style.width = boxW;
364
364
  }
365
365
  elements.push(<Box
366
- key={ix}
366
+ key={fieldName + '-' + ix}
367
367
  className={columnClassName}
368
368
  style={style}
369
369
  >{element}</Box>);
@@ -467,7 +467,7 @@ function Form(props) {
467
467
  `}
468
468
  /> : null;
469
469
  return <HStack
470
- key={ix}
470
+ key={fieldName + '-HStack-' + ix}
471
471
  className={`
472
472
  Form-HStack1
473
473
  flex-${flex}
@@ -624,7 +624,7 @@ function Form(props) {
624
624
  itemDefaultsToPass = itemDefaults;
625
625
  }
626
626
  return <Element
627
- key={ix}
627
+ key={'column-Element-' + type + '-' + ix}
628
628
  title={title}
629
629
  {...defaultsToPass}
630
630
  {...itemDefaultsToPass}
@@ -697,7 +697,7 @@ function Form(props) {
697
697
  </VStack>;
698
698
  }
699
699
  }
700
- return <HStack key={ix} className="Form-HStack3 w-full px-2 pb-1">{element}</HStack>;
700
+ return <HStack key={'Form-HStack3-' + ix} className="Form-HStack3 w-full px-2 pb-1">{element}</HStack>;
701
701
  }
702
702
 
703
703
 
@@ -908,7 +908,7 @@ function Form(props) {
908
908
  `}
909
909
  /> : null;
910
910
  return <HStack
911
- key={ix}
911
+ key={'Controller-HStack-' + ix}
912
912
  className={`
913
913
  Form-HStack11
914
914
  min-h-[50px]
@@ -930,6 +930,7 @@ function Form(props) {
930
930
 
931
931
  // add the "scroll to top" button
932
932
  getAncillaryButtons().push({
933
+ key: 'scrollToTop',
933
934
  icon: ArrowUp,
934
935
  reference: 'scrollToTop',
935
936
  onPress: () => scrollToAncillaryItem(0),
@@ -938,14 +939,15 @@ function Form(props) {
938
939
 
939
940
  _.each(ancillaryItems, (item, ix) => {
940
941
  let {
941
- type,
942
- title = null,
943
- description = null,
944
- icon,
945
- selectorId,
946
- selectorSelectedField,
947
- ...itemPropsToPass
948
- } = item;
942
+ type,
943
+ title = null,
944
+ description = null,
945
+ icon,
946
+ selectorId,
947
+ selectorSelectedField,
948
+ ...itemPropsToPass
949
+ } = item,
950
+ titleElement;
949
951
  if (isMultiple && type !== 'Attachments') {
950
952
  return;
951
953
  }
@@ -953,6 +955,7 @@ function Form(props) {
953
955
  // NOTE: this assumes that if one Ancillary item has an icon, they all do.
954
956
  // If they don't, the ix will be wrong!
955
957
  getAncillaryButtons().push({
958
+ key: 'ancillaryBtn-' + ix,
956
959
  icon,
957
960
  onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
958
961
  tooltip: title,
@@ -977,15 +980,15 @@ function Form(props) {
977
980
  if (record?.displayValue) {
978
981
  title += ' for ' + record.displayValue;
979
982
  }
980
- title = <Text
981
- className={`
982
- Form-Ancillary-Title
983
- font-bold
984
- ${styles.FORM_ANCILLARY_TITLE_CLASSNAME}
985
- `}
986
- >{title}</Text>;
983
+ titleElement = <Text
984
+ className={`
985
+ Form-Ancillary-Title
986
+ font-bold
987
+ ${styles.FORM_ANCILLARY_TITLE_CLASSNAME}
988
+ `}
989
+ >{title}</Text>;
987
990
  if (icon) {
988
- title = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{title}</HStack>
991
+ titleElement = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{titleElement}</HStack>
989
992
  }
990
993
  }
991
994
  if (description) {
@@ -1006,7 +1009,7 @@ function Form(props) {
1006
1009
  my-3
1007
1010
  `}
1008
1011
  >
1009
- {title}
1012
+ {titleElement}
1010
1013
  {description}
1011
1014
  {element}
1012
1015
  </VStack>);
@@ -1369,13 +1372,15 @@ function Form(props) {
1369
1372
  text={submitBtnLabel || 'Submit'}
1370
1373
  />}
1371
1374
 
1372
- {additionalFooterButtons && _.map(additionalFooterButtons, (props) => {
1375
+ {additionalFooterButtons && _.map(additionalFooterButtons, (props, ix) => {
1373
1376
  let isDisabled = false;
1374
1377
  if (props.disableOnInvalid) {
1375
1378
  isDisabled = !formState.isValid;
1376
1379
  }
1380
+ const key = 'additionalFooterBtn-' + ix;
1377
1381
  return <Button
1378
- {...testProps('additionalFooterBtn-' + props.key)}
1382
+ {...testProps(key)}
1383
+ key={key}
1379
1384
  {...props}
1380
1385
  onPress={(e) => handleSubmit(props.onPress, onSubmitError)(e)}
1381
1386
  icon={props.icon || null}
@@ -138,13 +138,6 @@ function GridComponent(props) {
138
138
  canColumnsSort = true,
139
139
  canColumnsReorder = true,
140
140
  canColumnsResize = true,
141
- canRowsReorder = false,
142
- areRowsDragSource = false,
143
- rowDragSourceType,
144
- getRowDragSourceItem,
145
- areRowsDropTarget = false,
146
- dropTargetAccept,
147
- onRowDrop,
148
141
  allowToggleSelection = false, // i.e. single click with no shift key toggles the selection of the item clicked on
149
142
  disableBottomToolbar = false,
150
143
  disablePagination = false,
@@ -175,6 +168,18 @@ function GridComponent(props) {
175
168
  noSelectorMeansNoResults = false,
176
169
  disableSelectorSelected = false,
177
170
 
171
+ // DND
172
+ canRowsReorder = false,
173
+ canRowAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
174
+ getCustomDragProxy, // optional fn to render custom drag preview: (item, selection) => ReactElement
175
+ dragPreviewOptions, // optional object for drag preview positioning options
176
+ areRowsDragSource = false,
177
+ rowDragSourceType,
178
+ getRowDragSourceItem,
179
+ areRowsDropTarget = false,
180
+ dropTargetAccept,
181
+ onRowDrop,
182
+
178
183
  // withComponent
179
184
  self,
180
185
 
@@ -549,10 +554,21 @@ function GridComponent(props) {
549
554
  } else {
550
555
  rowDragProps.dragSourceItem = {
551
556
  id: item.id,
557
+ item,
552
558
  getSelection,
553
559
  type: rowDragSourceType,
554
560
  };
555
561
  }
562
+
563
+ // Add custom drag preview options
564
+ if (dragPreviewOptions) {
565
+ rowDragProps.dragPreviewOptions = dragPreviewOptions;
566
+ }
567
+
568
+ // Add drag preview rendering
569
+ rowDragProps.getDragProxy = getCustomDragProxy ?
570
+ (dragItem) => getCustomDragProxy(item, getSelection()) :
571
+ null; // Let GlobalDragProxy handle the default case
556
572
  }
557
573
  if (areRowsDropTarget) {
558
574
  WhichRow = DropTargetGridRow;
@@ -562,6 +578,14 @@ function GridComponent(props) {
562
578
  // NOTE: item is sometimes getting destroyed, but it still as the id, so you can still use it
563
579
  onRowDrop(item, droppedItem); // item is what it was dropped on; droppedItem is the dragSourceItem defined above
564
580
  };
581
+ rowDragProps.canDrop = (droppedItem, monitor) => {
582
+ // Check if the drop operation would be valid based on business rules
583
+ if (canRowAcceptDrop && typeof canRowAcceptDrop === 'function') {
584
+ return canRowAcceptDrop(item, droppedItem);
585
+ }
586
+ // Default: allow all drops
587
+ return true;
588
+ };
565
589
  }
566
590
  if (areRowsDragSource && areRowsDropTarget) {
567
591
  WhichRow = DragSourceDropTargetGridRow;
@@ -1,4 +1,4 @@
1
- import { useMemo, } from 'react';
1
+ import { useMemo, useEffect, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  HStack,
@@ -8,7 +8,10 @@ import {
8
8
  } from '@project-components/Gluestack';
9
9
  import {
10
10
  UI_MODE_WEB,
11
+ UI_MODE_NATIVE,
12
+ CURRENT_MODE,
11
13
  } from '../../Constants/UiModes.js';
14
+ import { getEmptyImage } from 'react-dnd-html5-backend';
12
15
  import * as colourMixer from '@k-renwick/colour-mixer';
13
16
  import getComponentFromType from '../../Functions/getComponentFromType.js';
14
17
  import UiGlobals from '../../UiGlobals.js';
@@ -43,14 +46,28 @@ function GridRow(props) {
43
46
  isDraggable = false, // withDraggable
44
47
  isDragSource = false, // withDnd
45
48
  isOver = false, // drop target
49
+ canDrop,
50
+ draggedItem,
51
+ validateDrop, // same as canDrop (for visual feedback)
52
+ getDragProxy,
46
53
  dragSourceRef,
54
+ dragPreviewRef,
47
55
  dropTargetRef,
56
+ ...propsToPass
48
57
  } = props,
49
58
  styles = UiGlobals.styles;
50
59
 
51
60
  if (item.isDestroyed) {
52
61
  return null;
53
62
  }
63
+
64
+ // Hide the default drag preview only when using custom drag proxy (and only on web)
65
+ useEffect(() => {
66
+ if (dragPreviewRef && typeof dragPreviewRef === 'function' && getDragProxy && CURRENT_MODE === UI_MODE_WEB) {
67
+ // Only suppress default drag preview when we have a custom one and we're on web
68
+ dragPreviewRef(getEmptyImage(), { captureDraggingState: true });
69
+ }
70
+ }, [dragPreviewRef, getDragProxy]);
54
71
 
55
72
  const
56
73
  isPhantom = item.isPhantom,
@@ -59,6 +76,15 @@ function GridRow(props) {
59
76
 
60
77
  let bg = rowProps.bg || props.bg || styles.GRID_ROW_BG,
61
78
  mixWith;
79
+
80
+ // TODO: Finish Drop styling
81
+
82
+ // Use custom validation for enhanced visual feedback, fallback to React DnD's canDrop
83
+ let actualCanDrop = canDrop;
84
+ if (isOver && draggedItem && validateDrop) {
85
+ actualCanDrop = validateDrop(draggedItem);
86
+ }
87
+
62
88
  if (isRowSelectable && isSelected) {
63
89
  if (showHovers && isHovered) {
64
90
  mixWith = styles.GRID_ROW_SELECTED_BG_HOVER;
@@ -368,7 +394,11 @@ function GridRow(props) {
368
394
  isHovered,
369
395
  isOver,
370
396
  index,
397
+ canDrop,
398
+ draggedItem,
399
+ validateDrop,
371
400
  dragSourceRef,
401
+ dragPreviewRef,
372
402
  dropTargetRef,
373
403
  ]);
374
404
  }
@@ -1,4 +1,4 @@
1
- import { forwardRef, useState, } from 'react';
1
+ import { forwardRef, useState, useRef, } from 'react';
2
2
  import {
3
3
  HORIZONTAL,
4
4
  VERTICAL,
@@ -50,6 +50,7 @@ export default function withDraggable(WrappedComponent) {
50
50
  [isDragging, setIsDraggingRaw] = useState(false),
51
51
  [node, setNode] = useState(false),
52
52
  [bounds, setBounds] = useState(null),
53
+ nodeRef = useRef(null), // to get around React Draggable bug // https://stackoverflow.com/a/63603903
53
54
  { block } = useBlocking(),
54
55
  setIsDragging = (value) => {
55
56
  setIsDraggingRaw(value);
@@ -231,10 +232,11 @@ export default function withDraggable(WrappedComponent) {
231
232
  onDrag={handleDrag}
232
233
  onStop={handleStop}
233
234
  position={{ x: 0, y: 0, /* reset to dropped position */ }}
235
+ nodeRef={nodeRef}
234
236
  // bounds={bounds}
235
237
  {...draggableProps}
236
238
  >
237
- <div className="nsResize">
239
+ <div ref={nodeRef} className="nsResize">
238
240
  <WrappedComponent {...propsToPass} ref={ref} />
239
241
  </div>
240
242
  </Draggable>;
@@ -246,9 +248,10 @@ export default function withDraggable(WrappedComponent) {
246
248
  onStop={handleStop}
247
249
  position={{ x: 0, y: 0, /* reset to dropped position */ }}
248
250
  // bounds={bounds}
251
+ nodeRef={nodeRef}
249
252
  {...draggableProps}
250
253
  >
251
- <div className="ewResize" style={{ height: '100%', }}>
254
+ <div ref={nodeRef} className="ewResize" style={{ height: '100%', }}>
252
255
  <WrappedComponent {...propsToPass} ref={ref} />
253
256
  </div>
254
257
  </Draggable>;
@@ -262,9 +265,10 @@ export default function withDraggable(WrappedComponent) {
262
265
  onStop={handleStop}
263
266
  position={{ x: 0, y: 0, /* reset to dropped position */ }}
264
267
  handle={handle}
268
+ nodeRef={nodeRef}
265
269
  {...draggableProps}
266
270
  >
267
- <WrappedComponent {...propsToPass} ref={ref} />
271
+ <WrappedComponent {...propsToPass} ref={nodeRef} />
268
272
  </Draggable>;
269
273
  } else if (CURRENT_MODE === UI_MODE_NATIVE) {
270
274
 
@@ -670,8 +670,14 @@ export default function withEditor(WrappedComponent, isTree = false) {
670
670
  useEffect(() => {
671
671
  setEditorMode(calculateEditorMode());
672
672
 
673
- setIsIgnoreNextSelectionChange(false);
674
673
  setLastSelection(selection);
674
+
675
+ // Push isIgnoreNextSelectionChange until after a microtask to ensure all
676
+ // synchronous operations (including listener callbacks) are complete
677
+ // (this is to prevent the editor from immediately switching modes on doAdd in Tree)
678
+ Promise.resolve().then(() => {
679
+ setIsIgnoreNextSelectionChange(false);
680
+ });
675
681
  }, [selection]);
676
682
 
677
683
  if (self) {
@@ -54,8 +54,6 @@ import Xmark from '../Icons/Xmark.js';
54
54
  import Dot from '../Icons/Dot.js';
55
55
  import Collapse from '../Icons/Collapse.js';
56
56
  import Expand from '../Icons/Expand.js';
57
- import FolderClosed from '../Icons/FolderClosed.js';
58
- import FolderOpen from '../Icons/FolderOpen.js';
59
57
  import Gear from '../Icons/Gear.js';
60
58
  import MagnifyingGlass from '../Icons/MagnifyingGlass.js';
61
59
  import PaginationToolbar from '../Toolbar/PaginationToolbar.js';
@@ -93,21 +91,9 @@ function TreeComponent(props) {
93
91
  getDisplayTextFromSearchResults = (item) => {
94
92
  return item.id
95
93
  },
96
- getNodeIcon = (which, item) => { // decides what icon to show for this node
94
+ getNodeIcon = (item) => {
97
95
  // TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
98
- let icon;
99
- switch(which) {
100
- case COLLAPSED:
101
- icon = FolderClosed;
102
- break;
103
- case EXPANDED:
104
- icon = FolderOpen;
105
- break;
106
- case LEAF:
107
- icon = Dot;
108
- break;
109
- }
110
- return icon;
96
+ return Dot;
111
97
  },
112
98
  getNodeProps = (item) => {
113
99
  return {};
@@ -119,18 +105,6 @@ function TreeComponent(props) {
119
105
  showSelectHandle = true,
120
106
  isNodeSelectable = true,
121
107
  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,
134
108
  allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the node clicked on
135
109
  disableBottomToolbar = false,
136
110
  bottomToolbar = null,
@@ -142,11 +116,24 @@ function TreeComponent(props) {
142
116
  canRecordBeEdited,
143
117
  onTreeLoad,
144
118
  onLayout,
145
-
146
119
  selectorId,
147
120
  selectorSelected,
148
121
  selectorSelectedField = 'id',
149
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
+
150
137
  // withComponent
151
138
  self,
152
139
 
@@ -547,9 +534,7 @@ function TreeComponent(props) {
547
534
  treeRef,
548
535
  text: getNodeText(treeNode),
549
536
  content: getNodeContent ? getNodeContent(treeNode) : null,
550
- iconCollapsed: getNodeIcon(COLLAPSED, treeNode),
551
- iconExpanded: getNodeIcon(EXPANDED, treeNode),
552
- iconLeaf: getNodeIcon(LEAF, treeNode),
537
+ icon: getNodeIcon(treeNode),
553
538
  isExpanded: treeNode.isExpanded || defaultToExpanded || isRoot, // all non-root treeNodes are collapsed by default
554
539
  isVisible: isRoot ? areRootsVisible : true,
555
540
  isLoading: false,
@@ -587,6 +572,35 @@ function TreeComponent(props) {
587
572
  }
588
573
  return treeNodeData;
589
574
  },
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
+ }
582
+
583
+ const parentDatum = getDatumById(entity.parent.id);
584
+ if (!parentDatum) {
585
+ // Parent not found in current tree structure, rebuild
586
+ buildAndSetTreeNodeData();
587
+ return;
588
+ }
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);
593
+
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();
603
+ },
590
604
  datumContainsSelection = (datum) => {
591
605
  if (_.isEmpty(selection)) {
592
606
  return false;
@@ -1110,8 +1124,8 @@ function TreeComponent(props) {
1110
1124
  nodeDragProps.dropTargetAccept = dropTargetAccept;
1111
1125
 
1112
1126
  // Define validation logic once for reuse
1113
- const validateDrop = (droppedItem) => {
1114
- if (!droppedItem) {
1127
+ const validateDrop = (draggedItem) => {
1128
+ if (!draggedItem) {
1115
1129
  return false;
1116
1130
  }
1117
1131
 
@@ -1119,10 +1133,10 @@ function TreeComponent(props) {
1119
1133
 
1120
1134
  // Always include the dragged item itself in validation
1121
1135
  // If no selection exists, the dragged item is what we're moving
1122
- const nodesToValidate = currentSelection.length > 0 ? currentSelection : [droppedItem.item];
1136
+ const nodesToValidate = currentSelection.length > 0 ? currentSelection : [draggedItem.item];
1123
1137
 
1124
1138
  // validate that the dropped item is not already a direct child of the target node
1125
- if (isChildOf(droppedItem.item, item)) {
1139
+ if (isChildOf(draggedItem.item, item)) {
1126
1140
  return false;
1127
1141
  }
1128
1142
 
@@ -1140,13 +1154,13 @@ function TreeComponent(props) {
1140
1154
 
1141
1155
  if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1142
1156
  // custom business logic
1143
- return canNodeAcceptDrop(item, droppedItem);
1157
+ return canNodeAcceptDrop(item, draggedItem);
1144
1158
  }
1145
1159
  return true;
1146
1160
  };
1147
1161
 
1148
1162
  // Use the validation function for React DnD
1149
- nodeDragProps.canDrop = (droppedItem, monitor) => validateDrop(droppedItem);
1163
+ nodeDragProps.canDrop = (draggedItem, monitor) => validateDrop(draggedItem);
1150
1164
 
1151
1165
  // Pass the same validation function for visual feedback
1152
1166
  nodeDragProps.validateDrop = validateDrop;
@@ -1167,6 +1181,7 @@ function TreeComponent(props) {
1167
1181
  } else {
1168
1182
  nodeDragProps.dragSourceItem = {
1169
1183
  id: item.id,
1184
+ item,
1170
1185
  getSelection,
1171
1186
  type: nodeDragSourceType,
1172
1187
  };
@@ -1197,6 +1212,26 @@ function TreeComponent(props) {
1197
1212
  // Default: allow external drops
1198
1213
  return true;
1199
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
+
1200
1235
  nodeDragProps.onDrop = (droppedItem) => {
1201
1236
  // NOTE: item is sometimes getting destroyed, but it still has the id, so you can still use it
1202
1237
  onNodeDrop(item, droppedItem);
@@ -1274,7 +1309,7 @@ function TreeComponent(props) {
1274
1309
  Repository.on('load', setFalse);
1275
1310
  Repository.on('loadRootNodes', setFalse);
1276
1311
  Repository.on('loadRootNodes', buildAndSetTreeNodeData);
1277
- Repository.on('add', buildAndSetTreeNodeData);
1312
+ Repository.on('add', buildAndSetOneTreeNodeData);
1278
1313
  Repository.on('changeFilters', reloadTree);
1279
1314
  Repository.on('changeSorters', reloadTree);
1280
1315
 
@@ -1290,7 +1325,7 @@ function TreeComponent(props) {
1290
1325
  Repository.off('load', setFalse);
1291
1326
  Repository.off('loadRootNodes', setFalse);
1292
1327
  Repository.off('loadRootNodes', buildAndSetTreeNodeData);
1293
- Repository.off('add', buildAndSetTreeNodeData);
1328
+ Repository.off('add', buildAndSetOneTreeNodeData);
1294
1329
  Repository.off('changeFilters', reloadTree);
1295
1330
  Repository.off('changeSorters', reloadTree);
1296
1331
  };
@@ -18,6 +18,8 @@ import IconButton from '../Buttons/IconButton.js';
18
18
  import { withDragSource, withDropTarget } from '../Hoc/withDnd.js';
19
19
  import TreeNodeDragHandle from './TreeNodeDragHandle.js';
20
20
  import testProps from '../../Functions/testProps.js';
21
+ import ChevronRight from '../Icons/ChevronRight.js';
22
+ import ChevronDown from '../Icons/ChevronDown.js';
21
23
  import _ from 'lodash';
22
24
 
23
25
  // This was broken out from Tree simply so we can memoize it
@@ -51,9 +53,7 @@ export default function TreeNode(props) {
51
53
  depth = item.depth,
52
54
  text = datum.text,
53
55
  content = datum.content,
54
- iconCollapsed = datum.iconCollapsed,
55
- iconExpanded = datum.iconExpanded,
56
- iconLeaf = datum.iconLeaf,
56
+ icon = datum.icon,
57
57
  hash = item?.hash || item;
58
58
 
59
59
  // Hide the default drag preview only when using custom drag proxy (and only on web)
@@ -65,7 +65,6 @@ export default function TreeNode(props) {
65
65
  }, [dragPreviewRef, getDragProxy]);
66
66
 
67
67
  return useMemo(() => {
68
- const icon = hasChildren ? (isExpanded ? iconExpanded : iconCollapsed) : iconLeaf;
69
68
  let bg = props.nodeProps?.bg || props.bg || styles.TREE_NODE_BG,
70
69
  mixWith;
71
70
 
@@ -148,18 +147,19 @@ export default function TreeNode(props) {
148
147
  {isPhantom && <Box t={0} l={0} className="absolute bg-[#f00] h-[2px] w-[2px]" />}
149
148
 
150
149
  {isDragSource && <TreeNodeDragHandle />}
150
+
151
+ {hasChildren && <IconButton
152
+ {...testProps('expandBtn')}
153
+ icon={isExpanded ? ChevronDown : ChevronRight}
154
+ onPress={(e) => onToggle(datum, e)}
155
+ className="ml-2"
156
+ />}
151
157
 
152
- {isLoading ?
153
- <Spinner className="px-2" /> :
154
- (icon && hasChildren ?
155
- <IconButton
156
- {...testProps('expandBtn')}
157
- icon={icon}
158
- onPress={(e) => onToggle(datum, e)}
159
- /> :
160
- <Icon as={icon} className="ml-4 mr-1" />)}
158
+ {isLoading && <Spinner className="px-2" />}
159
+
160
+ {!isLoading && icon && <Icon as={icon} className="ml-2 mr-1" />}
161
161
 
162
- {text ? <TextNative
162
+ {text && <TextNative
163
163
  numberOfLines={1}
164
164
  ellipsizeMode="head"
165
165
  // {...propsToPass}
@@ -176,7 +176,7 @@ export default function TreeNode(props) {
176
176
  style={{
177
177
  userSelect: 'none',
178
178
  }}
179
- >{text}</TextNative> : null}
179
+ >{text}</TextNative>}
180
180
 
181
181
  {content}
182
182
 
@@ -6,9 +6,22 @@ import styles from '../../Styles/StyleSheets.js';
6
6
  import GripVertical from '../Icons/GripVertical.js';
7
7
 
8
8
  function TreeNodeDragHandle(props) {
9
+ let className = `
10
+ TreeNodeDragHandle
11
+ h-full
12
+ w-[14px]
13
+ px-[2px]
14
+ border-l-2
15
+ items-center
16
+ justify-center
17
+ select-none
18
+ `;
19
+ if (props.className) {
20
+ className += ' ' + props.className;
21
+ }
9
22
  return <VStack
10
23
  style={styles.ewResize}
11
- className="TreeNodeDragHandle h-full w-[14px] px-[2px] border-l-2 items-center justify-center select-none"
24
+ className={className}
12
25
  >
13
26
  <Icon
14
27
  as={GripVertical}