@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.
- package/package.json +1 -1
- package/src/Components/Editor/AttachmentDirectoriesEditor.js +51 -0
- package/src/Components/Editor/AttachmentsEditor.js +81 -0
- package/src/Components/Form/Field/Combo/AttachmentDirectoriesCombo.js +20 -0
- package/src/Components/Form/Field/Combo/AttachmentDirectoriesComboEditor.js +22 -0
- package/src/Components/Form/Field/Combo/AttachmentsCombo.js +20 -0
- package/src/Components/Form/Field/Combo/AttachmentsComboEditor.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentDirectoriesTag.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentDirectoriesTagEditor.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentsTag.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentsTagEditor.js +22 -0
- package/src/Components/Form/Form.js +52 -18
- package/src/Components/Grid/AttachmentDirectoriesFilteredGrid.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesFilteredGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesFilteredInlineGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesFilteredSideGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesGrid.js +20 -0
- package/src/Components/Grid/AttachmentDirectoriesGridEditor.js +27 -0
- package/src/Components/Grid/AttachmentDirectoriesInlineGridEditor.js +25 -0
- package/src/Components/Grid/AttachmentDirectoriesSideGridEditor.js +24 -0
- package/src/Components/Grid/AttachmentsFilteredGrid.js +17 -0
- package/src/Components/Grid/AttachmentsFilteredGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentsFilteredInlineGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentsFilteredSideGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentsGrid.js +20 -0
- package/src/Components/Grid/AttachmentsGridEditor.js +27 -0
- package/src/Components/Grid/AttachmentsInlineGridEditor.js +25 -0
- package/src/Components/Grid/AttachmentsSideGridEditor.js +24 -0
- package/src/Components/Grid/Columns/AttachmentDirectoriesGridColumns.js +32 -0
- package/src/Components/Grid/Columns/AttachmentsGridColumns.js +133 -0
- package/src/Components/Grid/Grid.js +193 -20
- package/src/Components/Grid/GridHeaderRow.js +10 -17
- package/src/Components/Grid/GridRow.js +49 -22
- package/src/Components/Grid/RowHandle.js +8 -6
- package/src/Components/Hoc/withEditor.js +1 -1
- package/src/Components/Hoc/withPdfButtons.js +1 -1
- package/src/Components/Hoc/withSelection.js +26 -4
- package/src/Components/Layout/AsyncOperation.js +299 -195
- package/src/Components/Messages/GlobalModals.js +1 -2
- package/src/Components/Panel/Panel.js +14 -2
- package/src/Components/Panel/TabPanel.js +1 -1
- package/src/Components/Panel/TreePanel.js +1 -1
- package/src/Components/Report/Report.js +106 -17
- package/src/Components/Toolbar/PaginationToolbar.js +4 -3
- package/src/Components/Toolbar/Toolbar.js +6 -3
- package/src/Components/Tree/Tree.js +218 -147
- package/src/Components/Tree/TreeNode.js +20 -13
- package/src/Components/Window/AttachmentDirectoriesEditorWindow.js +34 -0
- package/src/Components/Window/AttachmentsEditorWindow.js +34 -0
- package/src/Components/index.js +92 -1
- package/src/Constants/Attachments.js +2 -0
- package/src/Constants/Dates.js +2 -2
- package/src/Constants/Progress.js +5 -1
- package/src/Models/Schemas/AttachmentDirectories.js +66 -0
- package/src/Models/Schemas/Attachments.js +88 -0
- package/src/Models/Slices/SystemSlice.js +220 -0
- package/src/PlatformImports/Web/Attachments.js +783 -145
- 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
|
-
|
|
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
|
-
|
|
519
|
-
const
|
|
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
|
-
|
|
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
|
|
1075
|
-
|
|
1076
|
-
|
|
1129
|
+
const
|
|
1130
|
+
getSelection = () => dragSelectionRef.current,
|
|
1131
|
+
userHasPermissionToDrag = (!canUser || canUser(EDIT));
|
|
1077
1132
|
if (userHasPermissionToDrag) {
|
|
1078
|
-
|
|
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
|
-
//
|
|
1081
|
-
if (
|
|
1082
|
-
// internal drag
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
nodeDragProps.isDragSource = !item.isRoot;
|
|
1086
|
-
|
|
1087
|
-
|
|
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:
|
|
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
|
-
//
|
|
1194
|
+
// Unified canDrag logic
|
|
1102
1195
|
nodeDragProps.canDrag = (monitor) => {
|
|
1103
1196
|
const currentSelection = getSelection();
|
|
1104
1197
|
|
|
1105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1231
|
+
nodeDragProps.dropTargetAccept = acceptedDropTypes;
|
|
1135
1232
|
|
|
1136
|
-
//
|
|
1233
|
+
// drop validation
|
|
1137
1234
|
const validateDrop = (draggedItem) => {
|
|
1138
1235
|
if (!draggedItem) {
|
|
1139
1236
|
return false;
|
|
1140
1237
|
}
|
|
1141
1238
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1244
|
+
if (isInternalDrop && shouldSupportInternalDrag) {
|
|
1245
|
+
// Internal drop validation
|
|
1246
|
+
const currentSelection = getSelection();
|
|
1152
1247
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1516
|
+
minHeight: '100%',
|
|
1456
1517
|
}}
|
|
1457
1518
|
>
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
75
|
-
|
|
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
|
-
{
|
|
163
|
+
{showNodeHandle &&
|
|
158
164
|
<RowHandle
|
|
159
165
|
ref={dragSourceRef}
|
|
160
166
|
isDragSource={isDragSource}
|
|
161
167
|
isDraggable={isDraggable}
|
|
162
|
-
|
|
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
|
+
}
|