@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.
- package/package.json +1 -1
- package/src/Components/Fab/DynamicFab.js +16 -10
- package/src/Components/Form/Form.js +32 -27
- package/src/Components/Grid/Grid.js +43 -10
- package/src/Components/Grid/GridHeaderRow.js +3 -3
- package/src/Components/Grid/GridRow.js +35 -4
- package/src/Components/Grid/RowDragHandle.js +1 -1
- package/src/Components/Grid/RowSelectHandle.js +3 -3
- package/src/Components/Hoc/withAlert.js +4 -0
- package/src/Components/Hoc/withDnd.js +54 -57
- package/src/Components/Hoc/withDraggable.js +8 -4
- package/src/Components/Hoc/withEditor.js +7 -1
- package/src/Components/Icons/Arcs.js +10 -0
- package/src/Components/Tree/Tree.js +365 -328
- package/src/Components/Tree/TreeNode.js +103 -29
- package/src/Components/Tree/TreeNodeDragHandle.js +33 -0
- package/src/Components/index.js +2 -0
- package/src/Constants/Styles.js +2 -0
- package/src/PlatformImports/Web/Attachments.js +19 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, } from 'react';
|
|
1
|
+
import { useMemo, useEffect, } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
4
|
HStackNative,
|
|
@@ -7,10 +7,19 @@ import {
|
|
|
7
7
|
TextNative,
|
|
8
8
|
} from '@project-components/Gluestack';
|
|
9
9
|
import * as colourMixer from '@k-renwick/colour-mixer';
|
|
10
|
+
import {
|
|
11
|
+
UI_MODE_WEB,
|
|
12
|
+
CURRENT_MODE,
|
|
13
|
+
} from '../../Constants/UiModes.js';
|
|
14
|
+
import { getEmptyImage } from 'react-dnd-html5-backend';
|
|
10
15
|
import UiGlobals from '../../UiGlobals.js';
|
|
11
16
|
import withDraggable from '../Hoc/withDraggable.js';
|
|
12
17
|
import IconButton from '../Buttons/IconButton.js';
|
|
18
|
+
import { withDragSource, withDropTarget } from '../Hoc/withDnd.js';
|
|
19
|
+
import TreeNodeDragHandle from './TreeNodeDragHandle.js';
|
|
13
20
|
import testProps from '../../Functions/testProps.js';
|
|
21
|
+
import ChevronRight from '../Icons/ChevronRight.js';
|
|
22
|
+
import ChevronDown from '../Icons/ChevronDown.js';
|
|
14
23
|
import _ from 'lodash';
|
|
15
24
|
|
|
16
25
|
// This was broken out from Tree simply so we can memoize it
|
|
@@ -20,32 +29,64 @@ export default function TreeNode(props) {
|
|
|
20
29
|
datum,
|
|
21
30
|
nodeProps = {},
|
|
22
31
|
onToggle,
|
|
23
|
-
|
|
32
|
+
bg,
|
|
33
|
+
isDragSource,
|
|
24
34
|
isHovered,
|
|
25
|
-
isDragMode,
|
|
26
35
|
isHighlighted,
|
|
27
|
-
|
|
36
|
+
isOver,
|
|
37
|
+
isSelected,
|
|
38
|
+
canDrop,
|
|
39
|
+
draggedItem,
|
|
40
|
+
validateDrop, // same as canDrop (for visual feedback)
|
|
41
|
+
getDragProxy,
|
|
42
|
+
dragSourceRef,
|
|
43
|
+
dragPreviewRef,
|
|
44
|
+
dropTargetRef,
|
|
28
45
|
...propsToPass
|
|
29
46
|
} = props,
|
|
30
47
|
styles = UiGlobals.styles,
|
|
31
48
|
item = datum.item,
|
|
32
|
-
isPhantom = item.isPhantom,
|
|
33
49
|
isExpanded = datum.isExpanded,
|
|
34
50
|
isLoading = datum.isLoading,
|
|
51
|
+
isPhantom = item.isPhantom,
|
|
35
52
|
hasChildren = item.hasChildren,
|
|
36
53
|
depth = item.depth,
|
|
37
54
|
text = datum.text,
|
|
38
55
|
content = datum.content,
|
|
39
|
-
|
|
40
|
-
iconExpanded = datum.iconExpanded,
|
|
41
|
-
iconLeaf = datum.iconLeaf,
|
|
56
|
+
icon = datum.icon,
|
|
42
57
|
hash = item?.hash || item;
|
|
43
58
|
|
|
59
|
+
// Hide the default drag preview only when using custom drag proxy (and only on web)
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (dragPreviewRef && typeof dragPreviewRef === 'function' && getDragProxy && CURRENT_MODE === UI_MODE_WEB) {
|
|
62
|
+
// Only suppress default drag preview when we have a custom one and we're on web
|
|
63
|
+
dragPreviewRef(getEmptyImage(), { captureDraggingState: true });
|
|
64
|
+
}
|
|
65
|
+
}, [dragPreviewRef, getDragProxy]);
|
|
66
|
+
|
|
44
67
|
return useMemo(() => {
|
|
45
|
-
const icon = hasChildren ? (isExpanded ? iconExpanded : iconCollapsed) : iconLeaf;
|
|
46
68
|
let bg = props.nodeProps?.bg || props.bg || styles.TREE_NODE_BG,
|
|
47
69
|
mixWith;
|
|
48
|
-
|
|
70
|
+
|
|
71
|
+
// Determine visual state priority (highest to lowest):
|
|
72
|
+
// 1. Drop target states (when being hovered during drag)
|
|
73
|
+
// 2. Selection states
|
|
74
|
+
// 3. Hover states
|
|
75
|
+
// 4. Highlighted state
|
|
76
|
+
|
|
77
|
+
// Use custom validation for enhanced visual feedback, fallback to React DnD's canDrop
|
|
78
|
+
let actualCanDrop = canDrop;
|
|
79
|
+
if (isOver && draggedItem && validateDrop) {
|
|
80
|
+
actualCanDrop = validateDrop(draggedItem);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isOver && actualCanDrop) {
|
|
84
|
+
// Valid drop target - show positive feedback
|
|
85
|
+
mixWith = styles.TREE_NODE_DROP_VALID_BG || '#4ade80'; // green-400 fallback
|
|
86
|
+
// } else if (isOver && actualCanDrop === false) {
|
|
87
|
+
// // Invalid drop target - show negative feedback
|
|
88
|
+
// mixWith = styles.TREE_NODE_DROP_INVALID_BG || '#f87171'; // red-400 fallback
|
|
89
|
+
} else if (isSelected) {
|
|
49
90
|
if (isHovered) {
|
|
50
91
|
mixWith = styles.TREE_NODE_SELECTED_BG_HOVER;
|
|
51
92
|
} else {
|
|
@@ -53,8 +94,7 @@ export default function TreeNode(props) {
|
|
|
53
94
|
}
|
|
54
95
|
} else if (isHovered) {
|
|
55
96
|
mixWith = styles.TREE_NODE_BG_HOVER;
|
|
56
|
-
}
|
|
57
|
-
if (isHighlighted) {
|
|
97
|
+
} else if (isHighlighted) {
|
|
58
98
|
mixWith = styles.TREE_NODE_HIGHLIGHTED_BG;
|
|
59
99
|
}
|
|
60
100
|
if (mixWith) {
|
|
@@ -70,7 +110,17 @@ export default function TreeNode(props) {
|
|
|
70
110
|
items-center
|
|
71
111
|
flex-1
|
|
72
112
|
grow-1
|
|
113
|
+
select-none
|
|
114
|
+
cursor-pointer
|
|
73
115
|
`;
|
|
116
|
+
|
|
117
|
+
// Add drop state classes for additional styling
|
|
118
|
+
if (isOver && actualCanDrop) {
|
|
119
|
+
className += ' TreeNode--dropValid border-2 border-green-400';
|
|
120
|
+
// } else if (isOver && actualCanDrop === false) {
|
|
121
|
+
// className += ' TreeNode--dropInvalid border-2 border-red-400';
|
|
122
|
+
}
|
|
123
|
+
|
|
74
124
|
if (props.className) {
|
|
75
125
|
className += ' ' + props.className;
|
|
76
126
|
}
|
|
@@ -83,20 +133,33 @@ export default function TreeNode(props) {
|
|
|
83
133
|
style={{
|
|
84
134
|
backgroundColor: bg,
|
|
85
135
|
}}
|
|
136
|
+
ref={(element) => {
|
|
137
|
+
// Attach both drag and drop refs to the same element
|
|
138
|
+
if (dragSourceRef && typeof dragSourceRef === 'function') {
|
|
139
|
+
dragSourceRef(element);
|
|
140
|
+
}
|
|
141
|
+
if (dropTargetRef && dropTargetRef.current !== undefined) {
|
|
142
|
+
// dropTargetRef is a ref object, not a callback
|
|
143
|
+
dropTargetRef.current = element;
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
86
146
|
>
|
|
87
147
|
{isPhantom && <Box t={0} l={0} className="absolute bg-[#f00] h-[2px] w-[2px]" />}
|
|
88
148
|
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
/>}
|
|
157
|
+
|
|
158
|
+
{isLoading && <Spinner className="px-2" />}
|
|
98
159
|
|
|
99
|
-
{
|
|
160
|
+
{!isLoading && icon && <Icon as={icon} className="ml-2 mr-1" />}
|
|
161
|
+
|
|
162
|
+
{text && <TextNative
|
|
100
163
|
numberOfLines={1}
|
|
101
164
|
ellipsizeMode="head"
|
|
102
165
|
// {...propsToPass}
|
|
@@ -107,12 +170,13 @@ export default function TreeNode(props) {
|
|
|
107
170
|
flex
|
|
108
171
|
flex-1
|
|
109
172
|
text-ellipsis
|
|
173
|
+
select-none
|
|
110
174
|
${styles.TREE_NODE_CLASSNAME}
|
|
111
175
|
`}
|
|
112
176
|
style={{
|
|
113
|
-
|
|
177
|
+
userSelect: 'none',
|
|
114
178
|
}}
|
|
115
|
-
>{text}</TextNative>
|
|
179
|
+
>{text}</TextNative>}
|
|
116
180
|
|
|
117
181
|
{content}
|
|
118
182
|
|
|
@@ -122,18 +186,24 @@ export default function TreeNode(props) {
|
|
|
122
186
|
bg,
|
|
123
187
|
item,
|
|
124
188
|
hash, // this is an easy way to determine if the data has changed and the item needs to be rerendered
|
|
125
|
-
|
|
126
|
-
isHighlighted,
|
|
127
|
-
isSelected,
|
|
128
|
-
isPhantom,
|
|
189
|
+
isDragSource,
|
|
129
190
|
isExpanded,
|
|
191
|
+
isHighlighted,
|
|
130
192
|
isLoading,
|
|
193
|
+
isOver,
|
|
194
|
+
isPhantom,
|
|
195
|
+
isSelected,
|
|
131
196
|
hasChildren,
|
|
132
197
|
depth,
|
|
133
198
|
text,
|
|
134
199
|
content,
|
|
135
200
|
onToggle,
|
|
136
|
-
|
|
201
|
+
canDrop,
|
|
202
|
+
draggedItem,
|
|
203
|
+
validateDrop,
|
|
204
|
+
dragSourceRef,
|
|
205
|
+
dragPreviewRef,
|
|
206
|
+
dropTargetRef,
|
|
137
207
|
]);
|
|
138
208
|
}
|
|
139
209
|
|
|
@@ -147,3 +217,7 @@ function withAdditionalProps(WrappedComponent) {
|
|
|
147
217
|
}
|
|
148
218
|
|
|
149
219
|
export const DraggableTreeNode = withAdditionalProps(withDraggable(TreeNode));
|
|
220
|
+
|
|
221
|
+
export const DragSourceTreeNode = withDragSource(TreeNode);
|
|
222
|
+
export const DropTargetTreeNode = withDropTarget(TreeNode);
|
|
223
|
+
export const DragSourceDropTargetTreeNode = withDropTarget(withDragSource(TreeNode));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Icon,
|
|
3
|
+
VStack,
|
|
4
|
+
} from '@project-components/Gluestack';
|
|
5
|
+
import styles from '../../Styles/StyleSheets.js';
|
|
6
|
+
import GripVertical from '../Icons/GripVertical.js';
|
|
7
|
+
|
|
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
|
+
}
|
|
22
|
+
return <VStack
|
|
23
|
+
style={styles.ewResize}
|
|
24
|
+
className={className}
|
|
25
|
+
>
|
|
26
|
+
<Icon
|
|
27
|
+
as={GripVertical}
|
|
28
|
+
size="xs"
|
|
29
|
+
className="handle w-full h-full text-[#ccc]" />
|
|
30
|
+
</VStack>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default TreeNodeDragHandle;
|
package/src/Components/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import AngleLeft from './Icons/AngleLeft.js';
|
|
|
8
8
|
import AngleRight from './Icons/AngleRight.js';
|
|
9
9
|
import AnglesLeft from './Icons/AnglesLeft.js';
|
|
10
10
|
import AnglesRight from './Icons/AnglesRight.js';
|
|
11
|
+
import Arcs from './Icons/Arcs.js';
|
|
11
12
|
import Asterisk from './Icons/Asterisk.js';
|
|
12
13
|
import ArrowPointer from './Icons/ArrowPointer.js';
|
|
13
14
|
import ArrowUp from './Icons/ArrowUp.js';
|
|
@@ -255,6 +256,7 @@ const components = {
|
|
|
255
256
|
AngleRight,
|
|
256
257
|
AnglesLeft,
|
|
257
258
|
AnglesRight,
|
|
259
|
+
Arcs,
|
|
258
260
|
Asterisk,
|
|
259
261
|
ArrowPointer,
|
|
260
262
|
ArrowUp,
|
package/src/Constants/Styles.js
CHANGED
|
@@ -126,6 +126,8 @@ const defaults = {
|
|
|
126
126
|
TREE_NODE_SELECTED_BG: '#ff0', // must be hex
|
|
127
127
|
TREE_NODE_SELECTED_BG_HOVER: '#cc0', // must be hex
|
|
128
128
|
TREE_NODE_HIGHLIGHTED_BG: '#0f0', // must be hex
|
|
129
|
+
TREE_NODE_DROP_VALID_BG: '#4ade80', // must be hex - green-400 for valid drop targets
|
|
130
|
+
TREE_NODE_DROP_INVALID_BG: '#f87171', // must be hex - red-400 for invalid drop targets
|
|
129
131
|
TOOLBAR_CLASSNAME: 'bg-grey-200',
|
|
130
132
|
TOOLBAR_ITEMS_COLOR: 'text-grey-800',
|
|
131
133
|
TOOLBAR_ITEMS_ICON_SIZE: 'sm',
|
|
@@ -118,7 +118,14 @@ function AttachmentsElement(props) {
|
|
|
118
118
|
[isReady, setIsReady] = useState(false),
|
|
119
119
|
[isUploading, setIsUploading] = useState(false),
|
|
120
120
|
[showAll, setShowAll] = useState(false),
|
|
121
|
-
|
|
121
|
+
setFilesRaw = useRef([]),
|
|
122
|
+
setFiles = (files) => {
|
|
123
|
+
setFilesRaw.current = files;
|
|
124
|
+
forceUpdate();
|
|
125
|
+
},
|
|
126
|
+
getFiles = () => {
|
|
127
|
+
return setFilesRaw.current;
|
|
128
|
+
},
|
|
122
129
|
buildFiles = () => {
|
|
123
130
|
const files = _.map(Repository.entities, (entity) => {
|
|
124
131
|
return {
|
|
@@ -201,7 +208,9 @@ function AttachmentsElement(props) {
|
|
|
201
208
|
}
|
|
202
209
|
},
|
|
203
210
|
onFileDelete = (id) => {
|
|
204
|
-
const
|
|
211
|
+
const
|
|
212
|
+
files = getFiles(),
|
|
213
|
+
file = _.find(files, { id });
|
|
205
214
|
if (confirmBeforeDelete) {
|
|
206
215
|
confirm('Are you sure you want to delete the file "' + file.name + '"?', () => doDelete(id));
|
|
207
216
|
} else {
|
|
@@ -219,6 +228,7 @@ function AttachmentsElement(props) {
|
|
|
219
228
|
}
|
|
220
229
|
},
|
|
221
230
|
buildModalBody = (url, id) => {
|
|
231
|
+
const files = getFiles();
|
|
222
232
|
// This method was abstracted out so showModal/onPrev/onNext can all use it.
|
|
223
233
|
// url comes from FileMosaic, which passes in imageUrl,
|
|
224
234
|
// whereas FileCardCustom passes in id.
|
|
@@ -316,7 +326,9 @@ function AttachmentsElement(props) {
|
|
|
316
326
|
});
|
|
317
327
|
},
|
|
318
328
|
doDelete = (id) => {
|
|
319
|
-
const
|
|
329
|
+
const
|
|
330
|
+
files = getFiles(),
|
|
331
|
+
file = Repository.getById(id);
|
|
320
332
|
if (file) {
|
|
321
333
|
// if the file exists in the repository, delete it there
|
|
322
334
|
Repository.deleteById(id);
|
|
@@ -409,7 +421,9 @@ function AttachmentsElement(props) {
|
|
|
409
421
|
}
|
|
410
422
|
|
|
411
423
|
if (self) {
|
|
412
|
-
self.
|
|
424
|
+
self.getFiles = getFiles;
|
|
425
|
+
self.setFiles = setFiles;
|
|
426
|
+
self.clearFiles = clearFiles;
|
|
413
427
|
}
|
|
414
428
|
|
|
415
429
|
if (canCrud) {
|
|
@@ -425,6 +439,7 @@ function AttachmentsElement(props) {
|
|
|
425
439
|
if (props.className) {
|
|
426
440
|
className += ' ' + props.className;
|
|
427
441
|
}
|
|
442
|
+
const files = getFiles();
|
|
428
443
|
let content = <VStack className={className}>
|
|
429
444
|
<HStack className="AttachmentsElement-HStack flex-wrap">
|
|
430
445
|
{files.length === 0 && <Text className="text-grey-600 italic">No files</Text>}
|