@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 +1 -1
- package/src/Components/Fab/DynamicFab.js +21 -29
- package/src/Components/Form/Form.js +32 -27
- package/src/Components/Grid/Grid.js +31 -7
- package/src/Components/Grid/GridRow.js +31 -1
- package/src/Components/Hoc/withDraggable.js +8 -4
- package/src/Components/Hoc/withEditor.js +7 -1
- package/src/Components/Tree/Tree.js +75 -40
- package/src/Components/Tree/TreeNode.js +15 -15
- package/src/Components/Tree/TreeNodeDragHandle.js +14 -1
package/package.json
CHANGED
|
@@ -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 =
|
|
26
|
+
[isExpanded, setIsExpanded] = useState(false),
|
|
31
27
|
toggleFab = useCallback(() => {
|
|
32
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
64
|
-
});
|
|
55
|
+
} = btnConfig;
|
|
56
|
+
|
|
57
|
+
if (!isExpanded) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
65
60
|
|
|
66
|
-
return <
|
|
61
|
+
return <Box
|
|
67
62
|
key={ix}
|
|
68
|
-
style={
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
+
setIsExpanded(false);
|
|
89
81
|
}
|
|
90
82
|
}}
|
|
91
83
|
{...btnConfigToPass}
|
|
92
84
|
/>
|
|
93
|
-
</
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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(
|
|
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={
|
|
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 = (
|
|
94
|
+
getNodeIcon = (item) => {
|
|
97
95
|
// TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
|
|
98
|
-
|
|
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
|
-
|
|
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 = (
|
|
1114
|
-
if (!
|
|
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 : [
|
|
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(
|
|
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,
|
|
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 = (
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
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>
|
|
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=
|
|
24
|
+
className={className}
|
|
12
25
|
>
|
|
13
26
|
<Icon
|
|
14
27
|
as={GripVertical}
|