@onehat/ui 0.4.76 → 0.4.78

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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/src/Components/Editor/AttachmentDirectoriesEditor.js +51 -0
  3. package/src/Components/Editor/AttachmentsEditor.js +81 -0
  4. package/src/Components/Form/Field/Combo/AttachmentDirectoriesCombo.js +20 -0
  5. package/src/Components/Form/Field/Combo/AttachmentDirectoriesComboEditor.js +22 -0
  6. package/src/Components/Form/Field/Combo/AttachmentsCombo.js +20 -0
  7. package/src/Components/Form/Field/Combo/AttachmentsComboEditor.js +22 -0
  8. package/src/Components/Form/Field/Tag/AttachmentDirectoriesTag.js +22 -0
  9. package/src/Components/Form/Field/Tag/AttachmentDirectoriesTagEditor.js +22 -0
  10. package/src/Components/Form/Field/Tag/AttachmentsTag.js +22 -0
  11. package/src/Components/Form/Field/Tag/AttachmentsTagEditor.js +22 -0
  12. package/src/Components/Form/Form.js +52 -18
  13. package/src/Components/Grid/AttachmentDirectoriesFilteredGrid.js +17 -0
  14. package/src/Components/Grid/AttachmentDirectoriesFilteredGridEditor.js +17 -0
  15. package/src/Components/Grid/AttachmentDirectoriesFilteredInlineGridEditor.js +17 -0
  16. package/src/Components/Grid/AttachmentDirectoriesFilteredSideGridEditor.js +17 -0
  17. package/src/Components/Grid/AttachmentDirectoriesGrid.js +20 -0
  18. package/src/Components/Grid/AttachmentDirectoriesGridEditor.js +27 -0
  19. package/src/Components/Grid/AttachmentDirectoriesInlineGridEditor.js +25 -0
  20. package/src/Components/Grid/AttachmentDirectoriesSideGridEditor.js +24 -0
  21. package/src/Components/Grid/AttachmentsFilteredGrid.js +17 -0
  22. package/src/Components/Grid/AttachmentsFilteredGridEditor.js +17 -0
  23. package/src/Components/Grid/AttachmentsFilteredInlineGridEditor.js +17 -0
  24. package/src/Components/Grid/AttachmentsFilteredSideGridEditor.js +17 -0
  25. package/src/Components/Grid/AttachmentsGrid.js +20 -0
  26. package/src/Components/Grid/AttachmentsGridEditor.js +27 -0
  27. package/src/Components/Grid/AttachmentsInlineGridEditor.js +25 -0
  28. package/src/Components/Grid/AttachmentsSideGridEditor.js +24 -0
  29. package/src/Components/Grid/Columns/AttachmentDirectoriesGridColumns.js +32 -0
  30. package/src/Components/Grid/Columns/AttachmentsGridColumns.js +133 -0
  31. package/src/Components/Grid/Grid.js +193 -20
  32. package/src/Components/Grid/GridHeaderRow.js +10 -17
  33. package/src/Components/Grid/GridRow.js +49 -22
  34. package/src/Components/Grid/RowHandle.js +8 -6
  35. package/src/Components/Hoc/withEditor.js +1 -1
  36. package/src/Components/Hoc/withPdfButtons.js +1 -1
  37. package/src/Components/Hoc/withSelection.js +26 -4
  38. package/src/Components/Layout/AsyncOperation.js +299 -195
  39. package/src/Components/Messages/GlobalModals.js +1 -2
  40. package/src/Components/Panel/Panel.js +14 -2
  41. package/src/Components/Panel/TabPanel.js +1 -1
  42. package/src/Components/Panel/TreePanel.js +1 -1
  43. package/src/Components/Report/Report.js +106 -17
  44. package/src/Components/Toolbar/PaginationToolbar.js +4 -3
  45. package/src/Components/Toolbar/Toolbar.js +6 -3
  46. package/src/Components/Tree/Tree.js +218 -147
  47. package/src/Components/Tree/TreeNode.js +20 -13
  48. package/src/Components/Window/AttachmentDirectoriesEditorWindow.js +34 -0
  49. package/src/Components/Window/AttachmentsEditorWindow.js +34 -0
  50. package/src/Components/index.js +92 -1
  51. package/src/Constants/Attachments.js +2 -0
  52. package/src/Constants/Dates.js +2 -2
  53. package/src/Constants/Progress.js +5 -1
  54. package/src/Models/Schemas/AttachmentDirectories.js +66 -0
  55. package/src/Models/Schemas/Attachments.js +88 -0
  56. package/src/Models/Slices/SystemSlice.js +220 -0
  57. package/src/PlatformImports/Web/Attachments.js +783 -145
  58. package/src/Styles/Global.css +7 -2
@@ -69,7 +69,8 @@ const
69
69
  SIMULATED_CLICK = 0,
70
70
  SINGLE_CLICK = 1,
71
71
  DOUBLE_CLICK = 2,
72
- TRIPLE_CLICK = 3;
72
+ TRIPLE_CLICK = 3,
73
+ TREE_NODE_INTERNAL = 'TREE_NODE_INTERNAL';
73
74
 
74
75
  // NOTE: If using TreeComponent with getCustomDragProxy, ensure that <GlobalDragProxy /> exists in App.js
75
76
 
@@ -77,6 +78,7 @@ function TreeComponent(props) {
77
78
  const {
78
79
  areRootsVisible = true,
79
80
  autoLoadRootNodes = true,
81
+ autoSelectRootNode = false,
80
82
  extraParams = {}, // Additional params to send with each request ( e.g. { order: 'Categories.name ASC' })
81
83
  isNodeTextConfigurable = false,
82
84
  editDisplaySettings, // fn
@@ -102,11 +104,15 @@ function TreeComponent(props) {
102
104
  noneFoundText,
103
105
  disableLoadingIndicator = false,
104
106
  disableSelectorSelected = false,
107
+ hideReloadBtn = false,
108
+ showHeaderToolbar = true,
105
109
  showHovers = true,
106
110
  showSelectHandle = true,
107
111
  isNodeSelectable = true,
108
112
  isNodeHoverable = true,
109
113
  allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the node clicked on
114
+ allowDeselectAll = true, // allow deselecting all nodes by clicking on empty space in tree
115
+ forceSelectionOnCollapse = false, // when true, maintains selection by auto-selecting appropriate nodes when collapse would hide current selection
110
116
  disableBottomToolbar = false,
111
117
  bottomToolbar = null,
112
118
  topToolbar = null,
@@ -185,6 +191,9 @@ function TreeComponent(props) {
185
191
  selectRangeTo,
186
192
  isInSelection,
187
193
  noSelectorMeansNoResults = false,
194
+ disableSelectionChanges,
195
+ enableSelectionChanges,
196
+ refreshSelection,
188
197
 
189
198
  } = props,
190
199
  forceUpdate = useForceUpdate(),
@@ -197,7 +206,7 @@ function TreeComponent(props) {
197
206
  [searchFormData, setSearchFormData] = useState([]),
198
207
  [highlitedDatum, setHighlitedDatum] = useState(null),
199
208
  [treeSearchValue, setTreeSearchValue] = useState(''),
200
-
209
+ showNodeHandle = showSelectHandle || areNodesDragSource,
201
210
  // state getters & setters
202
211
  getTreeNodeData = () => {
203
212
  return treeNodeData.current;
@@ -378,7 +387,12 @@ function TreeComponent(props) {
378
387
  } else {
379
388
  // closing
380
389
  if (datumContainsSelection(datum)) {
381
- deselectAll();
390
+ if (forceSelectionOnCollapse) {
391
+ // Select the node being collapsed instead of deselecting all
392
+ setSelection([datum.item]);
393
+ } else {
394
+ deselectAll();
395
+ }
382
396
  }
383
397
  }
384
398
 
@@ -386,8 +400,24 @@ function TreeComponent(props) {
386
400
  },
387
401
  onCollapseAll = () => {
388
402
  const newTreeNodeData = [...getTreeNodeData()];
403
+
404
+ // Check if current selection will be hidden after collapse
405
+ let willSelectionBeHidden = false;
406
+ if (forceSelectionOnCollapse && selection.length > 0) {
407
+ // After collapse, only root nodes will be visible
408
+ const rootNodeIds = newTreeNodeData.map(datum => datum.item.id);
409
+ willSelectionBeHidden = !selection.some(selectedItem =>
410
+ rootNodeIds.includes(selectedItem.id)
411
+ );
412
+ }
413
+
389
414
  collapseNodes(newTreeNodeData);
390
415
  setTreeNodeData(newTreeNodeData);
416
+
417
+ // If selection will be hidden and we have root nodes, select the first root
418
+ if (willSelectionBeHidden && newTreeNodeData.length > 0) {
419
+ setSelection([newTreeNodeData[0].item]);
420
+ }
391
421
  },
392
422
  onExpandAll = () => {
393
423
  confirm('Are you sure you want to expand the whole tree? This may take a while.', async () => {
@@ -515,10 +545,34 @@ function TreeComponent(props) {
515
545
  return;
516
546
  }
517
547
 
518
- const selectedNode = selectedNodes[0];
519
- const commonAncestorId = await Repository.moveTreeNode(selectedNode, droppedOn.id);
548
+ // Check if drop target had no children before the move
549
+ const
550
+ hadNoChildrenBefore = !droppedOn.hasChildren,
551
+ selectedNode = selectedNodes[0],
552
+ commonAncestorId = await Repository.moveTreeNode(selectedNode, droppedOn.id);
520
553
  const commonAncestorDatum = getDatumById(commonAncestorId);
521
- reloadNode(commonAncestorDatum.item);
554
+
555
+ disableSelectionChanges(); // the reloadNode() commands change the selection. We don't want this when dragging and dropping the tree
556
+
557
+ await reloadNode(commonAncestorDatum.item); // **selectionChange to [] bc child node no longer exists
558
+
559
+ // If drop target gained its first children, reload it specifically and expand it
560
+ if (hadNoChildrenBefore) {
561
+ const refreshedDroppedOn = Repository.getById(droppedOn.id);
562
+ if (refreshedDroppedOn && refreshedDroppedOn.hasChildren) {
563
+ // Reload the drop target to get its new children
564
+ await reloadNode(refreshedDroppedOn); // **selectionChange
565
+
566
+ // Now expand it to show the moved node
567
+ const dropTargetDatum = getDatumById(refreshedDroppedOn.id);
568
+ if (dropTargetDatum) {
569
+ dropTargetDatum.isExpanded = true;
570
+ forceUpdate();
571
+ }
572
+ }
573
+ }
574
+ enableSelectionChanges();
575
+ refreshSelection();
522
576
 
523
577
  },
524
578
 
@@ -1068,28 +1122,59 @@ function TreeComponent(props) {
1068
1122
  }) => {
1069
1123
  const nodeDragProps = {};
1070
1124
  let WhichNode = TreeNode;
1125
+ nodeCanSelect = true,
1126
+ nodeCanDrag = false;
1071
1127
  if (CURRENT_MODE === UI_MODE_WEB) { // DND is currently web-only TODO: implement for RN
1072
- // Create a method that gets an always-current copy of the selection ids
1073
1128
  dragSelectionRef.current = selection;
1074
- const getSelection = () => dragSelectionRef.current;
1075
-
1076
- const userHasPermissionToDrag = (!canUser || canUser(EDIT));
1129
+ const
1130
+ getSelection = () => dragSelectionRef.current,
1131
+ userHasPermissionToDrag = (!canUser || canUser(EDIT));
1077
1132
  if (userHasPermissionToDrag) {
1078
- // NOTE: The Tree can either drag nodes internally or externally, but not both at the same time!
1133
+ const
1134
+ shouldSupportInternalDrag = canNodesMoveInternally,
1135
+ shouldSupportExternalDrag = areNodesDragSource,
1136
+ shouldSupportExternalDrop = areNodesDropTarget,
1137
+ supportedDragTypes = [],
1138
+ acceptedDropTypes = [];
1139
+
1140
+ if (shouldSupportInternalDrag) {
1141
+ supportedDragTypes.push(TREE_NODE_INTERNAL);
1142
+ }
1143
+ if (shouldSupportExternalDrag) {
1144
+ supportedDragTypes.push(nodeDragSourceType);
1145
+ }
1146
+ if (shouldSupportInternalDrag) {
1147
+ acceptedDropTypes.push(TREE_NODE_INTERNAL);
1148
+ }
1149
+ if (shouldSupportExternalDrop) {
1150
+ acceptedDropTypes.push(dropTargetAccept);
1151
+ }
1079
1152
 
1080
- // assign event handlers
1081
- if (canNodesMoveInternally) {
1082
- // internal drag/drop
1083
- const nodeDragSourceType = 'internal';
1084
- WhichNode = DragSourceDropTargetTreeNode;
1085
- nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
1086
- nodeDragProps.dragSourceType = nodeDragSourceType;
1087
- nodeDragProps.dragSourceItem = {
1153
+ // Set up drag source if needed
1154
+ if (supportedDragTypes.length > 0) {
1155
+ // If only internal drag is supported, root nodes cannot be dragged
1156
+ // since they cannot be dropped anywhere valid within the tree
1157
+ const canDragRootNode = shouldSupportExternalDrag || !shouldSupportInternalDrag;
1158
+ nodeDragProps.isDragSource = canDragRootNode || !item.isRoot;
1159
+
1160
+ // Use the primary drag type (external takes precedence if both are supported)
1161
+ // This is what react-dnd will use as the main type for drag operations
1162
+ nodeDragProps.dragSourceType = shouldSupportExternalDrag ? supportedDragTypes.find(type => type !== TREE_NODE_INTERNAL) || supportedDragTypes[0] : supportedDragTypes[0];
1163
+
1164
+ // Create unified drag source item
1165
+ const baseDragSourceItem = {
1088
1166
  id: item.id,
1089
1167
  item,
1090
1168
  getSelection,
1091
1169
  isInSelection,
1092
- type: nodeDragSourceType,
1170
+ type: nodeDragProps.dragSourceType, // Primary drag type
1171
+ supportedDragTypes, // Include all supported types
1172
+ sourceComponentRef: treeRef, // Reference to the originating component
1173
+ dragContext: {
1174
+ isInternal: shouldSupportInternalDrag,
1175
+ isExternal: shouldSupportExternalDrag,
1176
+ sourceComponent: treeRef
1177
+ },
1093
1178
  onDragStart: () => {
1094
1179
  if (!isInSelection(item)) { // get updated isSelected (will be stale if using one in closure)
1095
1180
  // reset the selection to just this one node if it's not already selected
@@ -1097,23 +1182,31 @@ function TreeComponent(props) {
1097
1182
  }
1098
1183
  },
1099
1184
  };
1185
+
1186
+ // Add external drag properties if needed
1187
+ if (shouldSupportExternalDrag && getNodeDragSourceItem) {
1188
+ const externalDragItem = getNodeDragSourceItem(item, getSelection, isInSelection, nodeDragSourceType);
1189
+ Object.assign(baseDragSourceItem, externalDragItem);
1190
+ }
1191
+
1192
+ nodeDragProps.dragSourceItem = baseDragSourceItem;
1100
1193
 
1101
- // Prevent root nodes from being dragged, and use custom logic if provided
1194
+ // Unified canDrag logic
1102
1195
  nodeDragProps.canDrag = (monitor) => {
1103
1196
  const currentSelection = getSelection();
1104
1197
 
1105
- if (isInSelection(item)) {
1106
- // make sure root node is not selected (can't drag root nodes)
1107
- const hasRootNode = currentSelection.some(node => node.isRoot);
1108
- if (hasRootNode) {
1109
- return false;
1110
- }
1111
- }
1198
+ // Root nodes can be dragged - restriction is handled in drop validation
1112
1199
 
1113
1200
  // Use custom drag validation if provided
1114
- if (canNodeMoveInternally) {
1201
+ if (shouldSupportInternalDrag && canNodeMoveInternally) {
1115
1202
  // In multi-selection, all nodes must be draggable
1116
- return currentSelection.every(node => canNodeMoveInternally(node));
1203
+ const internalValid = currentSelection.every(node => canNodeMoveInternally(node));
1204
+ if (!internalValid) return false;
1205
+ }
1206
+
1207
+ if (shouldSupportExternalDrag && canNodeMoveExternally) {
1208
+ const externalValid = canNodeMoveExternally(monitor);
1209
+ if (!externalValid) return false;
1117
1210
  }
1118
1211
 
1119
1212
  return true;
@@ -1129,134 +1222,92 @@ function TreeComponent(props) {
1129
1222
  (dragItem) => getCustomDragProxy(item, getSelection()) :
1130
1223
  null; // let GlobalDragProxy handle the default case
1131
1224
 
1132
- const dropTargetAccept = 'internal';
1225
+ nodeCanDrag = true;
1226
+ }
1227
+
1228
+ // Set up drop target if needed
1229
+ if (acceptedDropTypes.length > 0) {
1133
1230
  nodeDragProps.isDropTarget = true;
1134
- nodeDragProps.dropTargetAccept = dropTargetAccept;
1231
+ nodeDragProps.dropTargetAccept = acceptedDropTypes;
1135
1232
 
1136
- // Define validation logic once for reuse
1233
+ // drop validation
1137
1234
  const validateDrop = (draggedItem) => {
1138
1235
  if (!draggedItem) {
1139
1236
  return false;
1140
1237
  }
1141
1238
 
1142
- const currentSelection = getSelection();
1143
-
1144
- // Always include the dragged item itself in validation
1145
- // If no selection exists, the dragged item is what we're moving
1146
- const nodesToValidate = currentSelection.length > 0 ? currentSelection : [draggedItem.item];
1239
+ // Determine if this is an internal drop based on component reference
1240
+ // If sourceComponentRef is undefined, treat as external drop
1241
+ const isInternalDrop = draggedItem.sourceComponentRef &&
1242
+ draggedItem.sourceComponentRef === treeRef;
1147
1243
 
1148
- // validate that the dropped item is not already a direct child of the target node
1149
- if (isChildOf(draggedItem.item, item)) {
1150
- return false;
1151
- }
1244
+ if (isInternalDrop && shouldSupportInternalDrag) {
1245
+ // Internal drop validation
1246
+ const currentSelection = getSelection();
1152
1247
 
1153
- // Validate that none of the nodes being moved can be dropped into the target location
1154
- for (const nodeToMove of nodesToValidate) {
1155
- if (nodeToMove.id === item.id) {
1156
- // Cannot drop a node onto itself
1248
+ // Always include the dragged item itself in validation
1249
+ // If no selection exists, the dragged item is what we're moving
1250
+ const nodesToValidate = currentSelection.length > 0 ? currentSelection : [draggedItem.item];
1251
+
1252
+ // Root nodes cannot be moved internally within the same tree
1253
+ const hasRootNode = nodesToValidate.some(node => node.isRoot);
1254
+ if (hasRootNode) {
1157
1255
  return false;
1158
1256
  }
1159
- if (isDescendantOf(item, nodeToMove)) {
1160
- // Cannot drop a node into its own descendants
1257
+
1258
+ // validate that the dropped item is not already a direct child of the target node
1259
+ if (isChildOf(draggedItem.item, item)) {
1161
1260
  return false;
1162
1261
  }
1163
- }
1164
-
1165
- if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1166
- // custom business logic
1167
- return canNodeAcceptDrop(item, draggedItem);
1168
- }
1169
- return true;
1170
- };
1171
-
1172
- // Use the validation function for React DnD
1173
- nodeDragProps.canDrop = (draggedItem, monitor) => validateDrop(draggedItem);
1174
-
1175
- // Pass the same validation function for visual feedback
1176
- nodeDragProps.validateDrop = validateDrop;
1177
-
1178
- nodeDragProps.onDrop = (droppedItem) => {
1179
- if (belongsToThisTree(droppedItem)) {
1180
- onInternalNodeDrop(item, droppedItem);
1181
- }
1182
- };
1183
- } else {
1184
- // external drag/drop
1185
- if (areNodesDragSource) {
1186
- WhichNode = DragSourceTreeNode;
1187
- nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
1188
- nodeDragProps.dragSourceType = nodeDragSourceType;
1189
- if (getNodeDragSourceItem) {
1190
- nodeDragProps.dragSourceItem = getNodeDragSourceItem(item, getSelection, isInSelection, nodeDragSourceType);
1191
- } else {
1192
- nodeDragProps.dragSourceItem = {
1193
- id: item.id,
1194
- item,
1195
- getSelection,
1196
- isInSelection,
1197
- type: nodeDragSourceType,
1198
- };
1199
- }
1200
- nodeDragProps.dragSourceItem.onDragStart = () => {
1201
- if (!isInSelection(item)) { // get updated isSelected (will be stale if using one in closure)
1202
- // reset the selection to just this one node if it's not already selected
1203
- setSelection([item]);
1204
- }
1205
- };
1206
- if (canNodeMoveExternally) {
1207
- nodeDragProps.canDrag = canNodeMoveExternally;
1208
- }
1209
1262
 
1210
- // Add custom drag preview options
1211
- if (dragPreviewOptions) {
1212
- nodeDragProps.dragPreviewOptions = dragPreviewOptions;
1213
- }
1214
-
1215
- // Add drag preview rendering
1216
- nodeDragProps.getDragProxy = getCustomDragProxy ?
1217
- (dragItem) => getCustomDragProxy(item, getSelection()) :
1218
- null; // Let GlobalDragProxy handle the default case
1219
- }
1220
- if (areNodesDropTarget) {
1221
- WhichNode = DropTargetTreeNode;
1222
- nodeDragProps.isDropTarget = true;
1223
- nodeDragProps.dropTargetAccept = dropTargetAccept;
1224
- nodeDragProps.canDrop = (droppedItem, monitor) => {
1225
- // Check if the drop operation would be valid based on business rules
1226
- if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1227
- return canNodeAcceptDrop(item, droppedItem);
1228
- }
1229
- // Default: allow external drops
1230
- return true;
1231
- };
1232
-
1233
- // Define validation logic once for reuse
1234
- const validateDrop = (draggedItem) => {
1235
- if (!draggedItem) {
1236
- return false;
1263
+ // Validate that none of the nodes being moved can be dropped into the target location
1264
+ for (const nodeToMove of nodesToValidate) {
1265
+ if (nodeToMove.id === item.id) {
1266
+ // Cannot drop a node onto itself
1267
+ return false;
1268
+ }
1269
+ if (isDescendantOf(item, nodeToMove)) {
1270
+ // Cannot drop a node into its own descendants
1271
+ return false;
1272
+ }
1237
1273
  }
1238
1274
 
1275
+ // Internal drops are allowed if they pass the above validations
1276
+ return true;
1277
+ } else {
1278
+ // External drop validation - use custom business logic
1239
1279
  if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
1240
- // custom business logic
1241
1280
  return canNodeAcceptDrop(item, draggedItem);
1242
1281
  }
1282
+
1283
+ // Allow external drops by default if no custom validation is provided
1243
1284
  return true;
1244
- };
1245
-
1246
- // Use the validation function for React DnD
1247
- nodeDragProps.canDrop = (draggedItem, monitor) => validateDrop(draggedItem);
1285
+ }
1286
+ };
1287
+ nodeDragProps.canDrop = (draggedItem, monitor) => validateDrop(draggedItem); // for React DnD
1288
+ nodeDragProps.validateDrop = validateDrop; // for visual feedback
1289
+
1290
+ // drop handler
1291
+ nodeDragProps.onDrop = (droppedItem) => {
1292
+ // Determine if this is an internal drop based on component reference
1293
+ // If sourceComponentRef is undefined, treat as external drop
1294
+ const isInternalDrop = droppedItem.sourceComponentRef &&
1295
+ droppedItem.sourceComponentRef === treeRef;
1248
1296
 
1249
- // Pass the same validation function for visual feedback
1250
- nodeDragProps.validateDrop = validateDrop;
1251
-
1252
- nodeDragProps.onDrop = (droppedItem) => {
1253
- // NOTE: item is sometimes getting destroyed, but it still has the id, so you can still use it
1297
+ if (isInternalDrop && shouldSupportInternalDrag) {
1298
+ onInternalNodeDrop(item, droppedItem);
1299
+ } else if (shouldSupportExternalDrop && onNodeDrop) {
1254
1300
  onNodeDrop(item, droppedItem);
1255
- };
1256
- }
1257
- if (areNodesDragSource && areNodesDropTarget) {
1258
- WhichNode = DragSourceDropTargetTreeNode;
1259
- }
1301
+ }
1302
+ };
1303
+ }
1304
+
1305
+ if (nodeDragProps.isDragSource && nodeDragProps.isDropTarget) {
1306
+ WhichNode = DragSourceDropTargetTreeNode;
1307
+ } else if (nodeDragProps.isDragSource) {
1308
+ WhichNode = DragSourceTreeNode;
1309
+ } else if (nodeDragProps.isDropTarget) {
1310
+ WhichNode = DropTargetTreeNode;
1260
1311
  }
1261
1312
  }
1262
1313
  }
@@ -1270,7 +1321,9 @@ function TreeComponent(props) {
1270
1321
  isSelected={isSelected}
1271
1322
  isHovered={hovered}
1272
1323
  showHovers={showHovers}
1273
- showSelectHandle={showSelectHandle}
1324
+ showNodeHandle={showNodeHandle}
1325
+ nodeCanSelect={nodeCanSelect}
1326
+ nodeCanDrag={nodeCanDrag}
1274
1327
  isHighlighted={highlitedDatum === datum}
1275
1328
  {...nodeDragProps}
1276
1329
 
@@ -1334,6 +1387,12 @@ function TreeComponent(props) {
1334
1387
  if (autoLoadRootNodes) {
1335
1388
  await reloadTree();
1336
1389
  }
1390
+ if (autoSelectRootNode) {
1391
+ const rootNodes = Repository.getRootNodes();
1392
+ if (rootNodes.length) {
1393
+ setSelection([rootNodes[0]]);
1394
+ }
1395
+ }
1337
1396
  setIsReady(true);
1338
1397
  })();
1339
1398
 
@@ -1389,7 +1448,7 @@ function TreeComponent(props) {
1389
1448
  }
1390
1449
 
1391
1450
  const
1392
- headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue, getTreeNodeData()]),
1451
+ headerToolbarItemComponents = showHeaderToolbar ? useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue, getTreeNodeData()]) : null,
1393
1452
  footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, getTreeNodeData()]);
1394
1453
 
1395
1454
  if (!isReady) {
@@ -1411,7 +1470,7 @@ function TreeComponent(props) {
1411
1470
  />;
1412
1471
  } else if (footerToolbarItemComponents.length) {
1413
1472
  treeFooterComponent = <Toolbar>
1414
- <ReloadButton isTree={true} Repository={Repository} self={self} />
1473
+ {!hideReloadBtn && <ReloadButton isTree={true} Repository={Repository} self={self} />}
1415
1474
  {footerToolbarItemComponents}
1416
1475
  </Toolbar>;
1417
1476
  }
@@ -1444,22 +1503,34 @@ function TreeComponent(props) {
1444
1503
  <VStack
1445
1504
  ref={treeRef}
1446
1505
  onClick={() => {
1447
- deselectAll();
1506
+ if (allowDeselectAll) {
1507
+ deselectAll();
1508
+ }
1448
1509
  }}
1449
1510
  className="Tree-deselector w-full flex-1 p-1 bg-white"
1450
1511
  >
1451
1512
  <ScrollView
1452
- {...testProps('ScrollView')}
1513
+ {...testProps('ScrollView-Vertical')}
1453
1514
  className="Tree-ScrollView flex-1 w-full"
1454
1515
  contentContainerStyle={{
1455
- height: '100%',
1516
+ minHeight: '100%',
1456
1517
  }}
1457
1518
  >
1458
- {!treeNodes?.length ?
1459
- <CenterBox>
1460
- {Repository.isLoading ? <Loading /> : <NoRecordsFound text={noneFoundText} onRefresh={reloadTree} />}
1461
- </CenterBox> :
1462
- treeNodes}
1519
+ <ScrollView
1520
+ {...testProps('ScrollView-Horizontal')}
1521
+ horizontal={true}
1522
+ className="w-full"
1523
+ contentContainerStyle={{
1524
+ minWidth: '100%',
1525
+ flexDirection: 'column', // Keep vertical stacking
1526
+ }}
1527
+ >
1528
+ {!treeNodes?.length ?
1529
+ <CenterBox>
1530
+ {Repository.isLoading ? <Loading /> : <NoRecordsFound text={noneFoundText} onRefresh={reloadTree} />}
1531
+ </CenterBox> :
1532
+ treeNodes}
1533
+ </ScrollView>
1463
1534
  </ScrollView>
1464
1535
  </VStack>
1465
1536
 
@@ -23,18 +23,14 @@ import ChevronDown from '../Icons/ChevronDown.js';
23
23
  import _ from 'lodash';
24
24
 
25
25
  // Conditional import for web only
26
- let getEmptyImage;
27
- if (CURRENT_MODE === UI_MODE_WEB) {
28
- import('react-dnd-html5-backend').then((module) => {
29
- getEmptyImage = module.getEmptyImage;
30
- }).catch(() => {
31
- getEmptyImage = null;
32
- });
33
- }
26
+ let getEmptyImage = null;
34
27
 
35
28
  // This was broken out from Tree simply so we can memoize it
36
29
 
37
30
  export default function TreeNode(props) {
31
+ if (props.datum?.item?.isDestroyed) {
32
+ return null;
33
+ }
38
34
  const {
39
35
  datum,
40
36
  nodeProps = {},
@@ -46,7 +42,9 @@ export default function TreeNode(props) {
46
42
  isHighlighted,
47
43
  isOver,
48
44
  isSelected,
49
- showSelectHandle,
45
+ showNodeHandle,
46
+ nodeCanSelect,
47
+ nodeCanDrag,
50
48
  canDrop,
51
49
  draggedItem,
52
50
  validateDrop, // same as canDrop (for visual feedback)
@@ -68,11 +66,19 @@ export default function TreeNode(props) {
68
66
  icon = datum.icon,
69
67
  hash = item?.hash || item;
70
68
 
69
+
71
70
  // Hide the default drag preview only when using custom drag proxy (and only on web)
72
71
  useEffect(() => {
73
72
  if (dragPreviewRef && typeof dragPreviewRef === 'function' && getDragProxy && CURRENT_MODE === UI_MODE_WEB) {
74
- // Only suppress default drag preview when we have a custom one and we're on web
75
- dragPreviewRef(getEmptyImage(), { captureDraggingState: true });
73
+ // Load getEmptyImage dynamically and apply it
74
+ import('react-dnd-html5-backend').then((module) => {
75
+ const getEmptyImage = module.getEmptyImage;
76
+ if (getEmptyImage) {
77
+ dragPreviewRef(getEmptyImage(), { captureDraggingState: true });
78
+ }
79
+ }).catch((error) => {
80
+ console.warn('Failed to load react-dnd-html5-backend:', error);
81
+ });
76
82
  }
77
83
  }, [dragPreviewRef, getDragProxy]);
78
84
 
@@ -154,12 +160,13 @@ export default function TreeNode(props) {
154
160
  >
155
161
  {isPhantom && <Box className="absolute t-0 l-0 bg-[#f00] h-[2px] w-[2px]" />}
156
162
 
157
- {(isDragSource || showSelectHandle) &&
163
+ {showNodeHandle &&
158
164
  <RowHandle
159
165
  ref={dragSourceRef}
160
166
  isDragSource={isDragSource}
161
167
  isDraggable={isDraggable}
162
- showSelectHandle={showSelectHandle}
168
+ canSelect={nodeCanSelect}
169
+ canDrag={nodeCanDrag}
163
170
  />}
164
171
 
165
172
  {hasChildren && <IconButton
@@ -0,0 +1,34 @@
1
+ /**
2
+ * COPYRIGHT NOTICE
3
+ * This file is categorized as "Custom Source Code"
4
+ * and is subject to the terms and conditions defined in the
5
+ * "LICENSE.txt" file, which is part of this source code package.
6
+ */
7
+ import UiGlobals from '../../UiGlobals.js';
8
+ import Panel from '../Panel/Panel.js';
9
+ import useAdjustedWindowSize from '../../Hooks/useAdjustedWindowSize.js';
10
+ import AttachmentDirectoriesEditor from '../Editor/AttachmentDirectoriesEditor.js';
11
+
12
+ export default function AttachmentDirectoriesEditorWindow(props) {
13
+ const {
14
+ style, // prevent it being passed to Editor
15
+ ...propsToPass
16
+ } = props,
17
+ styles = UiGlobals.styles,
18
+ [width, height] = useAdjustedWindowSize(styles.DEFAULT_WINDOW_WIDTH, styles.DEFAULT_WINDOW_HEIGHT);
19
+
20
+ return <Panel
21
+ {...props}
22
+ reference="AttachmentDirectoriesEditorWindow"
23
+ isCollapsible={false}
24
+ model="AttachmentDirectories"
25
+ titleSuffix={props.editorMode === 'EDITOR_MODE__VIEW' || props.isEditorViewOnly ? ' Viewer' : ' Editor'}
26
+ className="AttachmentDirectoriesEditorWindow bg-white p-0"
27
+ isWindow={true}
28
+ w={width}
29
+ h={height}
30
+ flex={null}
31
+ >
32
+ <AttachmentDirectoriesEditor {...propsToPass} />
33
+ </Panel>;
34
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * COPYRIGHT NOTICE
3
+ * This file is categorized as "Custom Source Code"
4
+ * and is subject to the terms and conditions defined in the
5
+ * "LICENSE.txt" file, which is part of this source code package.
6
+ */
7
+ import UiGlobals from '../../UiGlobals.js';
8
+ import Panel from '../Panel/Panel.js';
9
+ import useAdjustedWindowSize from '../../Hooks/useAdjustedWindowSize.js';
10
+ import AttachmentsEditor from '../Editor/AttachmentsEditor.js';
11
+
12
+ export default function AttachmentsEditorWindow(props) {
13
+ const {
14
+ style, // prevent it being passed to Editor
15
+ ...propsToPass
16
+ } = props,
17
+ styles = UiGlobals.styles,
18
+ [width, height] = useAdjustedWindowSize(styles.DEFAULT_WINDOW_WIDTH, styles.DEFAULT_WINDOW_HEIGHT);
19
+
20
+ return <Panel
21
+ {...props}
22
+ reference="AttachmentsEditorWindow"
23
+ isCollapsible={false}
24
+ model="Attachments"
25
+ titleSuffix={props.editorMode === 'EDITOR_MODE__VIEW' || props.isEditorViewOnly ? ' Viewer' : ' Editor'}
26
+ className="AttachmentsEditorWindow bg-white p-0"
27
+ isWindow={true}
28
+ w={width}
29
+ h={height}
30
+ flex={null}
31
+ >
32
+ <AttachmentsEditor {...propsToPass} />
33
+ </Panel>;
34
+ }