@onehat/ui 0.4.64 → 0.4.66

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,9 +7,16 @@ 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';
14
21
  import _ from 'lodash';
15
22
 
@@ -20,18 +27,26 @@ export default function TreeNode(props) {
20
27
  datum,
21
28
  nodeProps = {},
22
29
  onToggle,
23
- isSelected,
30
+ bg,
31
+ isDragSource,
24
32
  isHovered,
25
- isDragMode,
26
33
  isHighlighted,
27
- bg,
34
+ isOver,
35
+ isSelected,
36
+ canDrop,
37
+ draggedItem,
38
+ validateDrop, // same as canDrop (for visual feedback)
39
+ getDragProxy,
40
+ dragSourceRef,
41
+ dragPreviewRef,
42
+ dropTargetRef,
28
43
  ...propsToPass
29
44
  } = props,
30
45
  styles = UiGlobals.styles,
31
46
  item = datum.item,
32
- isPhantom = item.isPhantom,
33
47
  isExpanded = datum.isExpanded,
34
48
  isLoading = datum.isLoading,
49
+ isPhantom = item.isPhantom,
35
50
  hasChildren = item.hasChildren,
36
51
  depth = item.depth,
37
52
  text = datum.text,
@@ -41,11 +56,38 @@ export default function TreeNode(props) {
41
56
  iconLeaf = datum.iconLeaf,
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
68
  const icon = hasChildren ? (isExpanded ? iconExpanded : iconCollapsed) : iconLeaf;
46
69
  let bg = props.nodeProps?.bg || props.bg || styles.TREE_NODE_BG,
47
70
  mixWith;
48
- if (isSelected) {
71
+
72
+ // Determine visual state priority (highest to lowest):
73
+ // 1. Drop target states (when being hovered during drag)
74
+ // 2. Selection states
75
+ // 3. Hover states
76
+ // 4. Highlighted state
77
+
78
+ // Use custom validation for enhanced visual feedback, fallback to React DnD's canDrop
79
+ let actualCanDrop = canDrop;
80
+ if (isOver && draggedItem && validateDrop) {
81
+ actualCanDrop = validateDrop(draggedItem);
82
+ }
83
+
84
+ if (isOver && actualCanDrop) {
85
+ // Valid drop target - show positive feedback
86
+ mixWith = styles.TREE_NODE_DROP_VALID_BG || '#4ade80'; // green-400 fallback
87
+ // } else if (isOver && actualCanDrop === false) {
88
+ // // Invalid drop target - show negative feedback
89
+ // mixWith = styles.TREE_NODE_DROP_INVALID_BG || '#f87171'; // red-400 fallback
90
+ } else if (isSelected) {
49
91
  if (isHovered) {
50
92
  mixWith = styles.TREE_NODE_SELECTED_BG_HOVER;
51
93
  } else {
@@ -53,8 +95,7 @@ export default function TreeNode(props) {
53
95
  }
54
96
  } else if (isHovered) {
55
97
  mixWith = styles.TREE_NODE_BG_HOVER;
56
- }
57
- if (isHighlighted) {
98
+ } else if (isHighlighted) {
58
99
  mixWith = styles.TREE_NODE_HIGHLIGHTED_BG;
59
100
  }
60
101
  if (mixWith) {
@@ -70,7 +111,17 @@ export default function TreeNode(props) {
70
111
  items-center
71
112
  flex-1
72
113
  grow-1
114
+ select-none
115
+ cursor-pointer
73
116
  `;
117
+
118
+ // Add drop state classes for additional styling
119
+ if (isOver && actualCanDrop) {
120
+ className += ' TreeNode--dropValid border-2 border-green-400';
121
+ // } else if (isOver && actualCanDrop === false) {
122
+ // className += ' TreeNode--dropInvalid border-2 border-red-400';
123
+ }
124
+
74
125
  if (props.className) {
75
126
  className += ' ' + props.className;
76
127
  }
@@ -83,12 +134,24 @@ export default function TreeNode(props) {
83
134
  style={{
84
135
  backgroundColor: bg,
85
136
  }}
137
+ ref={(element) => {
138
+ // Attach both drag and drop refs to the same element
139
+ if (dragSourceRef && typeof dragSourceRef === 'function') {
140
+ dragSourceRef(element);
141
+ }
142
+ if (dropTargetRef && dropTargetRef.current !== undefined) {
143
+ // dropTargetRef is a ref object, not a callback
144
+ dropTargetRef.current = element;
145
+ }
146
+ }}
86
147
  >
87
148
  {isPhantom && <Box t={0} l={0} className="absolute bg-[#f00] h-[2px] w-[2px]" />}
88
149
 
150
+ {isDragSource && <TreeNodeDragHandle />}
151
+
89
152
  {isLoading ?
90
153
  <Spinner className="px-2" /> :
91
- (icon && hasChildren && !isDragMode ?
154
+ (icon && hasChildren ?
92
155
  <IconButton
93
156
  {...testProps('expandBtn')}
94
157
  icon={icon}
@@ -107,10 +170,11 @@ 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
179
  >{text}</TextNative> : null}
116
180
 
@@ -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,20 @@
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
+ return <VStack
10
+ style={styles.ewResize}
11
+ className="TreeNodeDragHandle h-full w-[14px] px-[2px] border-l-2 items-center justify-center select-none"
12
+ >
13
+ <Icon
14
+ as={GripVertical}
15
+ size="xs"
16
+ className="handle w-full h-full text-[#ccc]" />
17
+ </VStack>;
18
+ }
19
+
20
+ export default TreeNodeDragHandle;
@@ -8,7 +8,9 @@ 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';
13
+ import ArrowPointer from './Icons/ArrowPointer.js';
12
14
  import ArrowUp from './Icons/ArrowUp.js';
13
15
  import Ban from './Icons/Ban.js';
14
16
  import Bars from './Icons/Bars.js';
@@ -204,6 +206,7 @@ import Color from './Form/Field/Color.js';
204
206
  import Combo from './Form/Field/Combo/Combo.js';
205
207
  // import { ComboEditor } from './Form/Field/Combo/Combo.js';
206
208
  import Container from './Container/Container.js';
209
+ import ContainerColumn from './Container/ContainerColumn.js';
207
210
  import DataMgt from './Screens/DataMgt.js';
208
211
  import Date from './Form/Field/Date.js';
209
212
  import DateRange from './Filter/DateRange.js';
@@ -253,7 +256,9 @@ const components = {
253
256
  AngleRight,
254
257
  AnglesLeft,
255
258
  AnglesRight,
259
+ Arcs,
256
260
  Asterisk,
261
+ ArrowPointer,
257
262
  ArrowUp,
258
263
  Ban,
259
264
  Bars,
@@ -449,6 +454,7 @@ const components = {
449
454
  Combo,
450
455
  // ComboEditor,
451
456
  Container,
457
+ ContainerColumn,
452
458
  DataMgt,
453
459
  Date,
454
460
  DateRange,
@@ -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>}