@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.
@@ -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
- isSelected,
32
+ bg,
33
+ isDragSource,
24
34
  isHovered,
25
- isDragMode,
26
35
  isHighlighted,
27
- bg,
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
- iconCollapsed = datum.iconCollapsed,
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
- if (isSelected) {
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
- {isLoading ?
90
- <Spinner className="px-2" /> :
91
- (icon && hasChildren && !isDragMode ?
92
- <IconButton
93
- {...testProps('expandBtn')}
94
- icon={icon}
95
- onPress={(e) => onToggle(datum, e)}
96
- /> :
97
- <Icon as={icon} className="ml-4 mr-1" />)}
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
- {text ? <TextNative
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
- // userSelect: 'none',
177
+ userSelect: 'none',
114
178
  }}
115
- >{text}</TextNative> : null}
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
- isDragMode,
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
- isLoading,
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;
@@ -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,
@@ -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
- [files, setFiles] = useState([]),
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 file = _.find(files, { id });
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 file = Repository.getById(id);
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.files = files;
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>}