@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.
- package/package.json +1 -1
- package/src/Components/Container/ContainerColumn.js +21 -0
- package/src/Components/Form/Field/Combo/Combo.js +1 -0
- package/src/Components/Form/Field/Json.js +3 -3
- package/src/Components/Grid/Grid.js +17 -5
- package/src/Components/Grid/GridHeaderRow.js +11 -1
- package/src/Components/Grid/GridRow.js +11 -3
- package/src/Components/Grid/RowDragHandle.js +4 -5
- package/src/Components/Grid/RowSelectHandle.js +18 -0
- package/src/Components/Hoc/withAlert.js +4 -0
- package/src/Components/Hoc/withDnd.js +54 -57
- package/src/Components/Icons/Arcs.js +10 -0
- package/src/Components/Icons/ArrowPointer.js +11 -0
- package/src/Components/Layout/AsyncOperation.js +36 -6
- package/src/Components/Report/Report.js +54 -0
- package/src/Components/Tree/Tree.js +312 -310
- package/src/Components/Tree/TreeNode.js +89 -15
- package/src/Components/Tree/TreeNodeDragHandle.js +20 -0
- package/src/Components/index.js +6 -0
- package/src/Constants/Styles.js +2 -0
- package/src/PlatformImports/Web/Attachments.js +19 -4
|
@@ -21,12 +21,18 @@ import {
|
|
|
21
21
|
EXPANDED,
|
|
22
22
|
LEAF,
|
|
23
23
|
} from '../../Constants/Tree.js';
|
|
24
|
+
import {
|
|
25
|
+
UI_MODE_WEB,
|
|
26
|
+
UI_MODE_NATIVE,
|
|
27
|
+
CURRENT_MODE,
|
|
28
|
+
} from '../../Constants/UiModes.js';
|
|
24
29
|
import UiGlobals from '../../UiGlobals.js';
|
|
25
30
|
import useForceUpdate from '../../Hooks/useForceUpdate.js';
|
|
26
31
|
import withContextMenu from '../Hoc/withContextMenu.js';
|
|
27
32
|
import withAlert from '../Hoc/withAlert.js';
|
|
28
33
|
import withComponent from '../Hoc/withComponent.js';
|
|
29
34
|
import withData from '../Hoc/withData.js';
|
|
35
|
+
import { withDropTarget } from '../Hoc/withDnd.js';
|
|
30
36
|
import withEvents from '../Hoc/withEvents.js';
|
|
31
37
|
import withSideEditor from '../Hoc/withSideEditor.js';
|
|
32
38
|
import withFilters from '../Hoc/withFilters.js';
|
|
@@ -41,7 +47,7 @@ import inArray from '../../Functions/inArray.js';
|
|
|
41
47
|
import testProps from '../../Functions/testProps.js';
|
|
42
48
|
import CenterBox from '../Layout/CenterBox.js';
|
|
43
49
|
import ReloadButton from '../Buttons/ReloadButton.js';
|
|
44
|
-
import TreeNode, {
|
|
50
|
+
import TreeNode, { DragSourceDropTargetTreeNode, DragSourceTreeNode, DropTargetTreeNode } from './TreeNode.js';
|
|
45
51
|
import FormPanel from '../Panel/FormPanel.js';
|
|
46
52
|
import Input from '../Form/Field/Input.js';
|
|
47
53
|
import Xmark from '../Icons/Xmark.js';
|
|
@@ -52,8 +58,6 @@ import FolderClosed from '../Icons/FolderClosed.js';
|
|
|
52
58
|
import FolderOpen from '../Icons/FolderOpen.js';
|
|
53
59
|
import Gear from '../Icons/Gear.js';
|
|
54
60
|
import MagnifyingGlass from '../Icons/MagnifyingGlass.js';
|
|
55
|
-
import NoReorderRows from '../Icons/NoReorderRows.js';
|
|
56
|
-
import ReorderRows from '../Icons/ReorderRows.js';
|
|
57
61
|
import PaginationToolbar from '../Toolbar/PaginationToolbar.js';
|
|
58
62
|
import NoRecordsFound from '../Grid/NoRecordsFound.js';
|
|
59
63
|
import Toolbar from '../Toolbar/Toolbar.js';
|
|
@@ -68,6 +72,8 @@ const
|
|
|
68
72
|
DOUBLE_CLICK = 2,
|
|
69
73
|
TRIPLE_CLICK = 3;
|
|
70
74
|
|
|
75
|
+
// NOTE: If using TreeComponent with getCustomDragProxy, ensure that <GlobalDragProxy /> exists in App.js
|
|
76
|
+
|
|
71
77
|
function TreeComponent(props) {
|
|
72
78
|
const {
|
|
73
79
|
areRootsVisible = true,
|
|
@@ -110,7 +116,21 @@ function TreeComponent(props) {
|
|
|
110
116
|
disableLoadingIndicator = false,
|
|
111
117
|
disableSelectorSelected = false,
|
|
112
118
|
showHovers = true,
|
|
113
|
-
|
|
119
|
+
showSelectHandle = true,
|
|
120
|
+
isNodeSelectable = true,
|
|
121
|
+
isNodeHoverable = true,
|
|
122
|
+
canNodesMoveInternally = false,
|
|
123
|
+
canNodeMoveInternally, // optional fn to customize whether each node can be dragged INternally
|
|
124
|
+
canNodeMoveExternally, // optional fn to customize whether each node can be dragged EXternally
|
|
125
|
+
canNodeAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
|
|
126
|
+
getCustomDragProxy, // optional fn to render custom drag preview: (item, selection) => ReactElement
|
|
127
|
+
dragPreviewOptions, // optional object for drag preview positioning options
|
|
128
|
+
areNodesDragSource = false,
|
|
129
|
+
nodeDragSourceType,
|
|
130
|
+
getNodeDragSourceItem,
|
|
131
|
+
areNodesDropTarget = false,
|
|
132
|
+
dropTargetAccept,
|
|
133
|
+
onNodeDrop,
|
|
114
134
|
allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the node clicked on
|
|
115
135
|
disableBottomToolbar = false,
|
|
116
136
|
bottomToolbar = null,
|
|
@@ -161,6 +181,12 @@ function TreeComponent(props) {
|
|
|
161
181
|
// withPermissions
|
|
162
182
|
canUser,
|
|
163
183
|
|
|
184
|
+
// withDnd
|
|
185
|
+
isDropTarget,
|
|
186
|
+
canDrop,
|
|
187
|
+
isOver,
|
|
188
|
+
dropTargetRef,
|
|
189
|
+
|
|
164
190
|
// withSelection
|
|
165
191
|
selection,
|
|
166
192
|
setSelection,
|
|
@@ -173,19 +199,15 @@ function TreeComponent(props) {
|
|
|
173
199
|
noSelectorMeansNoResults = false,
|
|
174
200
|
|
|
175
201
|
} = props,
|
|
176
|
-
styles = UiGlobals.styles,
|
|
177
202
|
forceUpdate = useForceUpdate(),
|
|
178
203
|
treeRef = useRef(),
|
|
179
204
|
treeNodeData = useRef(),
|
|
205
|
+
dragSelectionRef = useRef([]),
|
|
180
206
|
[isReady, setIsReady] = useState(false),
|
|
181
207
|
[isLoading, setIsLoading] = useState(false),
|
|
182
|
-
[rowToDatumMap, setRowToDatumMap] = useState({}),
|
|
183
208
|
[searchResults, setSearchResults] = useState([]),
|
|
184
209
|
[searchFormData, setSearchFormData] = useState([]),
|
|
185
210
|
[highlitedDatum, setHighlitedDatum] = useState(null),
|
|
186
|
-
[isDragMode, setIsDragMode] = useState(false),
|
|
187
|
-
[dragNodeId, setDragNodeId] = useState(null),
|
|
188
|
-
[dropRowIx, setDropRowIx] = useState(null),
|
|
189
211
|
[treeSearchValue, setTreeSearchValue] = useState(''),
|
|
190
212
|
|
|
191
213
|
// state getters & setters
|
|
@@ -270,7 +292,7 @@ function TreeComponent(props) {
|
|
|
270
292
|
}
|
|
271
293
|
const
|
|
272
294
|
parent = selection[0],
|
|
273
|
-
parentDatum =
|
|
295
|
+
parentDatum = getDatumById(parent.id);
|
|
274
296
|
|
|
275
297
|
if (parent.hasChildren && !parent.areChildrenLoaded) {
|
|
276
298
|
await loadChildren(parentDatum);
|
|
@@ -284,7 +306,7 @@ function TreeComponent(props) {
|
|
|
284
306
|
// Add the entity to the tree, show parent as hasChildren and expanded
|
|
285
307
|
const
|
|
286
308
|
parent = selection[0],
|
|
287
|
-
parentDatum =
|
|
309
|
+
parentDatum = getDatumById(parent.id);
|
|
288
310
|
if (!parent.hasChildren) {
|
|
289
311
|
parent.hasChildren = true; // since we're adding a new child
|
|
290
312
|
}
|
|
@@ -292,7 +314,6 @@ function TreeComponent(props) {
|
|
|
292
314
|
parentDatum.isExpanded = true;
|
|
293
315
|
}
|
|
294
316
|
|
|
295
|
-
buildRowToDatumMap();
|
|
296
317
|
forceUpdate();
|
|
297
318
|
},
|
|
298
319
|
onAfterAddSave = (entities) => {
|
|
@@ -307,7 +328,7 @@ function TreeComponent(props) {
|
|
|
307
328
|
// Refresh the node's display
|
|
308
329
|
const
|
|
309
330
|
node = entities[0],
|
|
310
|
-
existingDatum =
|
|
331
|
+
existingDatum = getDatumById(node.id), // TODO: Make this work for >1 entity
|
|
311
332
|
newDatum = buildTreeNodeDatum(node);
|
|
312
333
|
|
|
313
334
|
// copy the updated data to existingDatum
|
|
@@ -317,7 +338,7 @@ function TreeComponent(props) {
|
|
|
317
338
|
|
|
318
339
|
if (node.parent?.id) {
|
|
319
340
|
const
|
|
320
|
-
existingParentDatum =
|
|
341
|
+
existingParentDatum = getDatumById(node.parent.id),
|
|
321
342
|
newParentDatum = buildTreeNodeDatum(node.parent);
|
|
322
343
|
_.assign(existingParentDatum, newParentDatum);
|
|
323
344
|
existingParentDatum.isExpanded = true;
|
|
@@ -331,7 +352,7 @@ function TreeComponent(props) {
|
|
|
331
352
|
onBeforeSave = (entities) => {
|
|
332
353
|
const
|
|
333
354
|
node = entities[0],
|
|
334
|
-
datum =
|
|
355
|
+
datum = getDatumById(node.id); // TODO: Make this work for >1 entity
|
|
335
356
|
|
|
336
357
|
datum.isLoading = true;
|
|
337
358
|
forceUpdate();
|
|
@@ -339,9 +360,7 @@ function TreeComponent(props) {
|
|
|
339
360
|
onAfterDelete = async (entities) => {
|
|
340
361
|
const parent = entities[0].parent;
|
|
341
362
|
if (parent) {
|
|
342
|
-
await reloadNode(parent);
|
|
343
|
-
} else {
|
|
344
|
-
buildRowToDatumMap();
|
|
363
|
+
await reloadNode(parent);
|
|
345
364
|
}
|
|
346
365
|
},
|
|
347
366
|
onToggle = async (datum, e) => {
|
|
@@ -376,7 +395,6 @@ function TreeComponent(props) {
|
|
|
376
395
|
}
|
|
377
396
|
|
|
378
397
|
forceUpdate();
|
|
379
|
-
buildRowToDatumMap();
|
|
380
398
|
},
|
|
381
399
|
onCollapseAll = () => {
|
|
382
400
|
const newTreeNodeData = _.clone(getTreeNodeData());
|
|
@@ -473,35 +491,50 @@ function TreeComponent(props) {
|
|
|
473
491
|
});
|
|
474
492
|
},
|
|
475
493
|
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
if (!_.isEmpty(node.children)) {
|
|
483
|
-
let found1 = null;
|
|
484
|
-
_.each(node.children, (node2) => {
|
|
485
|
-
const found2 = findNodeById(node2);
|
|
486
|
-
if (found2) {
|
|
487
|
-
found1 = found2;
|
|
488
|
-
return false; // break loop
|
|
489
|
-
}
|
|
490
|
-
})
|
|
491
|
-
return found1
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
494
|
+
// internal DND
|
|
495
|
+
onInternalNodeDrop = async (droppedOn, droppedItem) => {
|
|
496
|
+
let selectedNodes = [];
|
|
497
|
+
if (droppedItem.getSelection) {
|
|
498
|
+
selectedNodes = droppedItem.getSelection();
|
|
494
499
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
500
|
+
if (_.isEmpty(selectedNodes)) {
|
|
501
|
+
selectedNodes = [droppedItem.item];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// filter out nodes that would already be moved by others in the selection
|
|
505
|
+
const selectedNodesClone = [...selectedNodes];
|
|
506
|
+
selectedNodes = selectedNodes.filter((node) => {
|
|
507
|
+
let isDescendant = false;
|
|
508
|
+
_.each(selectedNodesClone, (otherNode) => {
|
|
509
|
+
if (node.id === otherNode.id) {
|
|
510
|
+
return false; // skip self
|
|
511
|
+
}
|
|
512
|
+
isDescendant = isDescendantOf(node, otherNode);
|
|
513
|
+
if (isDescendant) {
|
|
514
|
+
return false; // found descendant; break loop
|
|
515
|
+
}
|
|
516
|
+
isDescendant = isDescendantOf(otherNode, node);
|
|
517
|
+
if (isDescendant) {
|
|
518
|
+
return false; // found ancestor; break loop
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
return !isDescendant;
|
|
502
522
|
});
|
|
503
|
-
|
|
523
|
+
|
|
524
|
+
const isMultiSelection = selectedNodes.length > 1;
|
|
525
|
+
if (isMultiSelection) {
|
|
526
|
+
alert('moving multiple disparate nodes not yet implemented');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const selectedNode = selectedNodes[0];
|
|
531
|
+
const commonAncestorId = await Repository.moveTreeNode(selectedNode, droppedOn.id);
|
|
532
|
+
const commonAncestorDatum = getDatumById(commonAncestorId);
|
|
533
|
+
reloadNode(commonAncestorDatum.item);
|
|
534
|
+
|
|
504
535
|
},
|
|
536
|
+
|
|
537
|
+
// utilities
|
|
505
538
|
buildTreeNodeDatum = (treeNode, defaultToExpanded = false) => {
|
|
506
539
|
// Build the data-representation of one node and its children,
|
|
507
540
|
// caching text & icon, keeping track of the state for whole tree
|
|
@@ -511,6 +544,7 @@ function TreeComponent(props) {
|
|
|
511
544
|
children = buildTreeNodeData(treeNode.children, defaultToExpanded), // recursively get data for children
|
|
512
545
|
datum = {
|
|
513
546
|
item: treeNode,
|
|
547
|
+
treeRef,
|
|
514
548
|
text: getNodeText(treeNode),
|
|
515
549
|
content: getNodeContent ? getNodeContent(treeNode) : null,
|
|
516
550
|
iconCollapsed: getNodeIcon(COLLAPSED, treeNode),
|
|
@@ -548,38 +582,11 @@ function TreeComponent(props) {
|
|
|
548
582
|
const treeNodeData = buildTreeNodeData(nodes);
|
|
549
583
|
setTreeNodeData(treeNodeData);
|
|
550
584
|
|
|
551
|
-
buildRowToDatumMap();
|
|
552
|
-
|
|
553
585
|
if (onTreeLoad) {
|
|
554
586
|
onTreeLoad(self);
|
|
555
587
|
}
|
|
556
588
|
return treeNodeData;
|
|
557
589
|
},
|
|
558
|
-
buildRowToDatumMap = () => {
|
|
559
|
-
const rowToDatumMap = {};
|
|
560
|
-
let ix = 0;
|
|
561
|
-
|
|
562
|
-
function walkTree(datum) {
|
|
563
|
-
if (!datum.isVisible) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Add this datum's id
|
|
568
|
-
rowToDatumMap[ix] = datum;
|
|
569
|
-
ix++;
|
|
570
|
-
|
|
571
|
-
if (datum.isExpanded) {
|
|
572
|
-
_.each(datum.children, (child) => {
|
|
573
|
-
walkTree(child);
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
_.each(getTreeNodeData(), (rootDatum) => {
|
|
578
|
-
walkTree(rootDatum);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
setRowToDatumMap(rowToDatumMap);
|
|
582
|
-
},
|
|
583
590
|
datumContainsSelection = (datum) => {
|
|
584
591
|
if (_.isEmpty(selection)) {
|
|
585
592
|
return false;
|
|
@@ -698,13 +705,38 @@ function TreeComponent(props) {
|
|
|
698
705
|
|
|
699
706
|
return treeNodes;
|
|
700
707
|
},
|
|
708
|
+
belongsToThisTree = (treeNode) => {
|
|
709
|
+
if (!treeNode) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
const datum = getDatumById(treeNode.id);
|
|
713
|
+
if (!datum) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
return datum.treeRef === treeRef;
|
|
717
|
+
},
|
|
718
|
+
isDescendantOf = (potentialDescendant, potentialAncestor) => {
|
|
719
|
+
// Check if potentialDescendant is a descendant of potentialAncestor
|
|
720
|
+
// by walking up the parent chain from potentialDescendant
|
|
721
|
+
let currentTreeNode = potentialDescendant;
|
|
722
|
+
while(currentTreeNode) {
|
|
723
|
+
if (currentTreeNode.id === potentialAncestor.id) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
currentTreeNode = currentTreeNode.parent;
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
},
|
|
730
|
+
isChildOf = (potentialChild, potentialParent) => {
|
|
731
|
+
return potentialChild.parent?.id === potentialParent.id;
|
|
732
|
+
},
|
|
701
733
|
reloadTree = () => {
|
|
702
734
|
Repository.areRootNodesLoaded = false;
|
|
703
735
|
return buildAndSetTreeNodeData();
|
|
704
736
|
},
|
|
705
737
|
reloadNode = async (node) => {
|
|
706
738
|
// mark node as loading
|
|
707
|
-
const existingDatum =
|
|
739
|
+
const existingDatum = getDatumById(node.id);
|
|
708
740
|
existingDatum.isLoading = true;
|
|
709
741
|
forceUpdate();
|
|
710
742
|
|
|
@@ -718,8 +750,6 @@ function TreeComponent(props) {
|
|
|
718
750
|
_.assign(existingDatum, _.omit(newDatum, ['isExpanded']));
|
|
719
751
|
existingDatum.isLoading = false;
|
|
720
752
|
forceUpdate();
|
|
721
|
-
|
|
722
|
-
buildRowToDatumMap();
|
|
723
753
|
},
|
|
724
754
|
loadChildren = async (datum, depth = 1) => {
|
|
725
755
|
|
|
@@ -755,12 +785,9 @@ function TreeComponent(props) {
|
|
|
755
785
|
// Hide loading indicator
|
|
756
786
|
datum.isLoading = false;
|
|
757
787
|
forceUpdate();
|
|
758
|
-
|
|
759
|
-
buildRowToDatumMap();
|
|
760
788
|
},
|
|
761
789
|
collapseNodes = (nodes) => {
|
|
762
790
|
collapseNodesRecursive(nodes);
|
|
763
|
-
buildRowToDatumMap();
|
|
764
791
|
},
|
|
765
792
|
collapseNodesRecursive = (nodes) => {
|
|
766
793
|
_.each(nodes, (node) => {
|
|
@@ -779,7 +806,6 @@ function TreeComponent(props) {
|
|
|
779
806
|
|
|
780
807
|
// expand them in UI
|
|
781
808
|
expandNodesRecursive(nodes);
|
|
782
|
-
buildRowToDatumMap();
|
|
783
809
|
},
|
|
784
810
|
expandNodesRecursive = (nodes) => {
|
|
785
811
|
_.each(nodes, (node) => {
|
|
@@ -846,7 +872,6 @@ function TreeComponent(props) {
|
|
|
846
872
|
}
|
|
847
873
|
|
|
848
874
|
setTreeNodeData(newTreeNodeData);
|
|
849
|
-
buildRowToDatumMap();
|
|
850
875
|
},
|
|
851
876
|
scrollToNode = (node) => {
|
|
852
877
|
// Helper for expandPath
|
|
@@ -907,17 +932,6 @@ function TreeComponent(props) {
|
|
|
907
932
|
isDisabled: false,
|
|
908
933
|
},
|
|
909
934
|
];
|
|
910
|
-
if (canNodesReorder) {
|
|
911
|
-
buttons.push({
|
|
912
|
-
key: 'reorderBtn',
|
|
913
|
-
text: (isDragMode ? 'Exit' : 'Enter') + ' reorder mode',
|
|
914
|
-
handler: () => {
|
|
915
|
-
setIsDragMode(!isDragMode);
|
|
916
|
-
},
|
|
917
|
-
icon: isDragMode ? NoReorderRows : ReorderRows,
|
|
918
|
-
isDisabled: false,
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
935
|
if (isNodeTextConfigurable && editDisplaySettings) {
|
|
922
936
|
buttons.push({
|
|
923
937
|
key: 'editNodeTextBtn',
|
|
@@ -978,9 +992,6 @@ function TreeComponent(props) {
|
|
|
978
992
|
if (e.preventDefault && e.cancelable) {
|
|
979
993
|
e.preventDefault();
|
|
980
994
|
}
|
|
981
|
-
if (isDragMode) {
|
|
982
|
-
return
|
|
983
|
-
}
|
|
984
995
|
switch (e.detail) {
|
|
985
996
|
case SIMULATED_CLICK:
|
|
986
997
|
case SINGLE_CLICK:
|
|
@@ -1014,9 +1025,6 @@ function TreeComponent(props) {
|
|
|
1014
1025
|
if (e.preventDefault && e.cancelable) {
|
|
1015
1026
|
e.preventDefault();
|
|
1016
1027
|
}
|
|
1017
|
-
if (isDragMode) {
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
1028
|
|
|
1021
1029
|
if (!setSelection) {
|
|
1022
1030
|
return;
|
|
@@ -1030,6 +1038,8 @@ function TreeComponent(props) {
|
|
|
1030
1038
|
}
|
|
1031
1039
|
}}
|
|
1032
1040
|
className={`
|
|
1041
|
+
Pressable
|
|
1042
|
+
Node
|
|
1033
1043
|
flex-row
|
|
1034
1044
|
`}
|
|
1035
1045
|
style={{
|
|
@@ -1041,32 +1051,176 @@ function TreeComponent(props) {
|
|
|
1041
1051
|
focused,
|
|
1042
1052
|
pressed,
|
|
1043
1053
|
}) => {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if (
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1054
|
+
const nodeDragProps = {};
|
|
1055
|
+
let WhichNode = TreeNode;
|
|
1056
|
+
if (CURRENT_MODE === UI_MODE_WEB) { // DND is currently web-only TODO: implement for RN
|
|
1057
|
+
// Create a method that gets an always-current copy of the selection ids
|
|
1058
|
+
dragSelectionRef.current = selection;
|
|
1059
|
+
const getSelection = () => dragSelectionRef.current;
|
|
1060
|
+
|
|
1061
|
+
const userHasPermissionToDrag = (!canUser || canUser(EDIT));
|
|
1062
|
+
if (userHasPermissionToDrag) {
|
|
1063
|
+
// NOTE: The Tree can either drag nodes internally or externally, but not both at the same time!
|
|
1064
|
+
|
|
1065
|
+
// assign event handlers
|
|
1066
|
+
if (canNodesMoveInternally) {
|
|
1067
|
+
// internal drag/drop
|
|
1068
|
+
const nodeDragSourceType = 'internal';
|
|
1069
|
+
WhichNode = DragSourceDropTargetTreeNode;
|
|
1070
|
+
nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
|
|
1071
|
+
nodeDragProps.dragSourceType = nodeDragSourceType;
|
|
1072
|
+
nodeDragProps.dragSourceItem = {
|
|
1073
|
+
id: item.id,
|
|
1074
|
+
item,
|
|
1075
|
+
getSelection,
|
|
1076
|
+
type: nodeDragSourceType,
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// Prevent root nodes from being dragged, and use custom logic if provided
|
|
1080
|
+
nodeDragProps.canDrag = (monitor) => {
|
|
1081
|
+
const currentSelection = getSelection();
|
|
1082
|
+
|
|
1083
|
+
// Check if any selected node is a root node (can't drag root nodes)
|
|
1084
|
+
const hasRootNode = currentSelection.some(node => node.isRoot);
|
|
1085
|
+
if (hasRootNode) {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Use custom drag validation if provided
|
|
1090
|
+
if (canNodeMoveInternally) {
|
|
1091
|
+
// In multi-selection, all nodes must be draggable
|
|
1092
|
+
return currentSelection.every(node => canNodeMoveInternally(node));
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return true;
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// Add custom drag preview options
|
|
1099
|
+
if (dragPreviewOptions) {
|
|
1100
|
+
nodeDragProps.dragPreviewOptions = dragPreviewOptions;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Add drag preview rendering
|
|
1104
|
+
nodeDragProps.getDragProxy = getCustomDragProxy ?
|
|
1105
|
+
(dragItem) => getCustomDragProxy(item, getSelection()) :
|
|
1106
|
+
null; // Let GlobalDragProxy handle the default case
|
|
1107
|
+
|
|
1108
|
+
const dropTargetAccept = 'internal';
|
|
1109
|
+
nodeDragProps.isDropTarget = true;
|
|
1110
|
+
nodeDragProps.dropTargetAccept = dropTargetAccept;
|
|
1111
|
+
|
|
1112
|
+
// Define validation logic once for reuse
|
|
1113
|
+
const validateDrop = (droppedItem) => {
|
|
1114
|
+
if (!droppedItem) {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const currentSelection = getSelection();
|
|
1119
|
+
|
|
1120
|
+
// Always include the dragged item itself in validation
|
|
1121
|
+
// If no selection exists, the dragged item is what we're moving
|
|
1122
|
+
const nodesToValidate = currentSelection.length > 0 ? currentSelection : [droppedItem.item];
|
|
1123
|
+
|
|
1124
|
+
// validate that the dropped item is not already a direct child of the target node
|
|
1125
|
+
if (isChildOf(droppedItem.item, item)) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Validate that none of the nodes being moved can be dropped into the target location
|
|
1130
|
+
for (const nodeToMove of nodesToValidate) {
|
|
1131
|
+
if (nodeToMove.id === item.id) {
|
|
1132
|
+
// Cannot drop a node onto itself
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
if (isDescendantOf(item, nodeToMove)) {
|
|
1136
|
+
// Cannot drop a node into its own descendants
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
|
|
1142
|
+
// custom business logic
|
|
1143
|
+
return canNodeAcceptDrop(item, droppedItem);
|
|
1144
|
+
}
|
|
1145
|
+
return true;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// Use the validation function for React DnD
|
|
1149
|
+
nodeDragProps.canDrop = (droppedItem, monitor) => validateDrop(droppedItem);
|
|
1150
|
+
|
|
1151
|
+
// Pass the same validation function for visual feedback
|
|
1152
|
+
nodeDragProps.validateDrop = validateDrop;
|
|
1153
|
+
|
|
1154
|
+
nodeDragProps.onDrop = (droppedItem) => {
|
|
1155
|
+
if (belongsToThisTree(droppedItem)) {
|
|
1156
|
+
onInternalNodeDrop(item, droppedItem);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
} else {
|
|
1160
|
+
// external drag/drop
|
|
1161
|
+
if (areNodesDragSource) {
|
|
1162
|
+
WhichNode = DragSourceTreeNode;
|
|
1163
|
+
nodeDragProps.isDragSource = !item.isRoot; // Root nodes cannot be dragged
|
|
1164
|
+
nodeDragProps.dragSourceType = nodeDragSourceType;
|
|
1165
|
+
if (getNodeDragSourceItem) {
|
|
1166
|
+
nodeDragProps.dragSourceItem = getNodeDragSourceItem(item, getSelection, nodeDragSourceType);
|
|
1167
|
+
} else {
|
|
1168
|
+
nodeDragProps.dragSourceItem = {
|
|
1169
|
+
id: item.id,
|
|
1170
|
+
getSelection,
|
|
1171
|
+
type: nodeDragSourceType,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
if (canNodeMoveExternally) {
|
|
1175
|
+
nodeDragProps.canDrag = canNodeMoveExternally;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Add custom drag preview options
|
|
1179
|
+
if (dragPreviewOptions) {
|
|
1180
|
+
nodeDragProps.dragPreviewOptions = dragPreviewOptions;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Add drag preview rendering
|
|
1184
|
+
nodeDragProps.getDragProxy = getCustomDragProxy ?
|
|
1185
|
+
(dragItem) => getCustomDragProxy(item, getSelection()) :
|
|
1186
|
+
null; // Let GlobalDragProxy handle the default case
|
|
1187
|
+
}
|
|
1188
|
+
if (areNodesDropTarget) {
|
|
1189
|
+
WhichNode = DropTargetTreeNode;
|
|
1190
|
+
nodeDragProps.isDropTarget = true;
|
|
1191
|
+
nodeDragProps.dropTargetAccept = dropTargetAccept;
|
|
1192
|
+
nodeDragProps.canDrop = (droppedItem, monitor) => {
|
|
1193
|
+
// Check if the drop operation would be valid based on business rules
|
|
1194
|
+
if (canNodeAcceptDrop && typeof canNodeAcceptDrop === 'function') {
|
|
1195
|
+
return canNodeAcceptDrop(item, droppedItem);
|
|
1196
|
+
}
|
|
1197
|
+
// Default: allow external drops
|
|
1198
|
+
return true;
|
|
1199
|
+
};
|
|
1200
|
+
nodeDragProps.onDrop = (droppedItem) => {
|
|
1201
|
+
// NOTE: item is sometimes getting destroyed, but it still has the id, so you can still use it
|
|
1202
|
+
onNodeDrop(item, droppedItem);
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
if (areNodesDragSource && areNodesDropTarget) {
|
|
1206
|
+
WhichNode = DragSourceDropTargetTreeNode;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1059
1210
|
}
|
|
1060
1211
|
|
|
1061
|
-
return <
|
|
1212
|
+
return <WhichNode
|
|
1062
1213
|
datum={datum}
|
|
1063
1214
|
nodeProps={nodeProps}
|
|
1064
1215
|
onToggle={onToggle}
|
|
1216
|
+
isNodeSelectable={isNodeSelectable}
|
|
1217
|
+
isNodeHoverable={isNodeHoverable}
|
|
1065
1218
|
isSelected={isSelected}
|
|
1066
1219
|
isHovered={hovered}
|
|
1067
|
-
|
|
1220
|
+
showHovers={showHovers}
|
|
1221
|
+
showSelectHandle={showSelectHandle}
|
|
1068
1222
|
isHighlighted={highlitedDatum === datum}
|
|
1069
|
-
{...
|
|
1223
|
+
{...nodeDragProps}
|
|
1070
1224
|
|
|
1071
1225
|
// fields={fields}
|
|
1072
1226
|
/>;
|
|
@@ -1088,154 +1242,6 @@ function TreeComponent(props) {
|
|
|
1088
1242
|
}
|
|
1089
1243
|
});
|
|
1090
1244
|
return nodes;
|
|
1091
|
-
},
|
|
1092
|
-
|
|
1093
|
-
// drag/drop
|
|
1094
|
-
getDragProxy = (node) => {
|
|
1095
|
-
|
|
1096
|
-
// TODO: Maybe the proxy should grab itself and all descendants??
|
|
1097
|
-
|
|
1098
|
-
const
|
|
1099
|
-
row = node,
|
|
1100
|
-
rowRect = row.getBoundingClientRect(),
|
|
1101
|
-
parent = row.parentElement,
|
|
1102
|
-
parentRect = parent.getBoundingClientRect(),
|
|
1103
|
-
proxy = row.cloneNode(true),
|
|
1104
|
-
top = rowRect.top - parentRect.top,
|
|
1105
|
-
rows = _.filter(parent.children, (childNode) => {
|
|
1106
|
-
if (childNode.getBoundingClientRect().height === 0 && childNode.style.visibility !== 'hidden') {
|
|
1107
|
-
return false; // Skip zero-height children
|
|
1108
|
-
}
|
|
1109
|
-
if (childNode === proxy) {
|
|
1110
|
-
return false;
|
|
1111
|
-
}
|
|
1112
|
-
return true;
|
|
1113
|
-
}),
|
|
1114
|
-
dragRowIx = Array.from(rows).indexOf(row),
|
|
1115
|
-
dragRowRecord = rowToDatumMap[dragRowIx].item;
|
|
1116
|
-
|
|
1117
|
-
setDragNodeId(dragRowRecord.id); // the id of which record is being dragged
|
|
1118
|
-
|
|
1119
|
-
proxy.style.top = top + 'px';
|
|
1120
|
-
proxy.style.left = (dragRowRecord.depth * DEPTH_INDENT_PX) + 'px';
|
|
1121
|
-
proxy.style.height = rowRect.height + 'px';
|
|
1122
|
-
proxy.style.width = rowRect.width + 'px';
|
|
1123
|
-
proxy.style.display = 'flex';
|
|
1124
|
-
proxy.style.position = 'absolute';
|
|
1125
|
-
proxy.style.border = '1px solid #bbb';
|
|
1126
|
-
return proxy;
|
|
1127
|
-
},
|
|
1128
|
-
onDrag = (info, e, proxy, node) => {
|
|
1129
|
-
// console.log('onDrag', info, e, proxy, node);
|
|
1130
|
-
const
|
|
1131
|
-
proxyRect = proxy.getBoundingClientRect(),
|
|
1132
|
-
row = node,
|
|
1133
|
-
parent = row.parentElement,
|
|
1134
|
-
parentRect = parent.getBoundingClientRect(),
|
|
1135
|
-
rows = _.filter(parent.children, (childNode) => {
|
|
1136
|
-
if (childNode.getBoundingClientRect().height === 0 && childNode.style.visibility !== 'hidden') {
|
|
1137
|
-
return false; // Skip zero-height children
|
|
1138
|
-
}
|
|
1139
|
-
if (childNode === proxy) {
|
|
1140
|
-
return false;
|
|
1141
|
-
}
|
|
1142
|
-
return true;
|
|
1143
|
-
}),
|
|
1144
|
-
currentY = proxyRect.top - parentRect.top; // top position of pointer, relative to page
|
|
1145
|
-
|
|
1146
|
-
// Figure out which row the user wants as a parentId
|
|
1147
|
-
let newIx = 0; // default to root being new parentId
|
|
1148
|
-
_.each(rows, (child, ix, all) => {
|
|
1149
|
-
const
|
|
1150
|
-
rect = child.getBoundingClientRect(), // rect of the row of this iteration
|
|
1151
|
-
{
|
|
1152
|
-
top,
|
|
1153
|
-
bottom,
|
|
1154
|
-
height,
|
|
1155
|
-
} = rect,
|
|
1156
|
-
compensatedBottom = bottom - parentRect.top;
|
|
1157
|
-
|
|
1158
|
-
if (child === proxy) {
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
if (ix === 0) {
|
|
1162
|
-
// first row
|
|
1163
|
-
if (currentY < compensatedBottom) {
|
|
1164
|
-
newIx = 0;
|
|
1165
|
-
return false;
|
|
1166
|
-
}
|
|
1167
|
-
return;
|
|
1168
|
-
} else if (ix === all.length -1) {
|
|
1169
|
-
// last row
|
|
1170
|
-
if (currentY < compensatedBottom) {
|
|
1171
|
-
newIx = ix;
|
|
1172
|
-
return false;
|
|
1173
|
-
}
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// all other rows
|
|
1178
|
-
if (currentY < compensatedBottom) {
|
|
1179
|
-
newIx = ix;
|
|
1180
|
-
return false;
|
|
1181
|
-
}
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const
|
|
1186
|
-
dragDatum = getDatumById(dragNodeId),
|
|
1187
|
-
dragDatumChildIds = getDatumChildIds(dragDatum),
|
|
1188
|
-
dropRowDatum = rowToDatumMap[newIx],
|
|
1189
|
-
dropRowRecord = dropRowDatum.item,
|
|
1190
|
-
dropNodeId = dropRowRecord.id,
|
|
1191
|
-
dragNodeContainsDropNode = inArray(dropNodeId, dragDatumChildIds) || dropRowRecord.id === dragNodeId;
|
|
1192
|
-
|
|
1193
|
-
if (dragNodeContainsDropNode) {
|
|
1194
|
-
// the node can be a child of any node except itself or its own descendants
|
|
1195
|
-
setDropRowIx(null);
|
|
1196
|
-
setHighlitedDatum(null);
|
|
1197
|
-
|
|
1198
|
-
} else {
|
|
1199
|
-
// console.log('setDropRowIx', newIx);
|
|
1200
|
-
setDropRowIx(newIx);
|
|
1201
|
-
|
|
1202
|
-
// highlight the drop node
|
|
1203
|
-
setHighlitedDatum(dropRowDatum);
|
|
1204
|
-
|
|
1205
|
-
// shift proxy's depth
|
|
1206
|
-
const depth = (dropRowRecord.id === dragNodeId) ? dropRowRecord.depth : dropRowRecord.depth + 1;
|
|
1207
|
-
proxy.style.left = (depth * DEPTH_INDENT_PX) + 'px';
|
|
1208
|
-
}
|
|
1209
|
-
},
|
|
1210
|
-
onDragStop = async (delta, e, config) => {
|
|
1211
|
-
// console.log('onDragStop', delta, e, config);
|
|
1212
|
-
|
|
1213
|
-
if (_.isNil(dropRowIx)) {
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
dragDatum = getDatumById(dragNodeId),
|
|
1219
|
-
dragRowRecord = dragDatum.item,
|
|
1220
|
-
dropRowDatum = rowToDatumMap[dropRowIx],
|
|
1221
|
-
dropRowRecord = dropRowDatum.item;
|
|
1222
|
-
|
|
1223
|
-
if (Repository) {
|
|
1224
|
-
if (!Repository.isDestroyed) {
|
|
1225
|
-
const commonAncestorId = await Repository.moveTreeNode(dragRowRecord, dropRowRecord.id);
|
|
1226
|
-
const commonAncestorDatum = getDatumById(commonAncestorId);
|
|
1227
|
-
reloadNode(commonAncestorDatum.item);
|
|
1228
|
-
}
|
|
1229
|
-
} else {
|
|
1230
|
-
|
|
1231
|
-
throw Error('Not yet implemented');
|
|
1232
|
-
// function arrayMove(arr, fromIndex, toIndex) {
|
|
1233
|
-
// var element = arr[fromIndex];
|
|
1234
|
-
// arr.splice(fromIndex, 1);
|
|
1235
|
-
// arr.splice(toIndex, 0, element);
|
|
1236
|
-
// }
|
|
1237
|
-
// arrayMove(data, dragNodeIx, finalDropIx);
|
|
1238
|
-
}
|
|
1239
1245
|
};
|
|
1240
1246
|
|
|
1241
1247
|
useEffect(() => {
|
|
@@ -1331,8 +1337,8 @@ function TreeComponent(props) {
|
|
|
1331
1337
|
}
|
|
1332
1338
|
|
|
1333
1339
|
const
|
|
1334
|
-
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue,
|
|
1335
|
-
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons,
|
|
1340
|
+
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [Repository?.hash, treeSearchValue, getTreeNodeData()]),
|
|
1341
|
+
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, getTreeNodeData()]);
|
|
1336
1342
|
|
|
1337
1343
|
if (!isReady) {
|
|
1338
1344
|
return <CenterBox>
|
|
@@ -1365,18 +1371,10 @@ function TreeComponent(props) {
|
|
|
1365
1371
|
w-full
|
|
1366
1372
|
min-w-[300px]
|
|
1367
1373
|
`;
|
|
1368
|
-
if (
|
|
1369
|
-
className +=
|
|
1370
|
-
${styles.GRID_REORDER_BORDER_COLOR}
|
|
1371
|
-
${styles.GRID_REORDER_BORDER_WIDTH}
|
|
1372
|
-
${styles.GRID_REORDER_BORDER_STYLE}
|
|
1373
|
-
`;
|
|
1374
|
+
if (isLoading) {
|
|
1375
|
+
className += ' border-t-2 border-[#f00]';
|
|
1374
1376
|
} else {
|
|
1375
|
-
|
|
1376
|
-
className += ' border-t-2 border-[#f00]';
|
|
1377
|
-
} else {
|
|
1378
|
-
className += ' border-t-1 border-grey-300';
|
|
1379
|
-
}
|
|
1377
|
+
className += ' border-t-1 border-grey-300';
|
|
1380
1378
|
}
|
|
1381
1379
|
if (props.className) {
|
|
1382
1380
|
className += ' ' + props.className;
|
|
@@ -1394,9 +1392,7 @@ function TreeComponent(props) {
|
|
|
1394
1392
|
<VStack
|
|
1395
1393
|
ref={treeRef}
|
|
1396
1394
|
onClick={() => {
|
|
1397
|
-
|
|
1398
|
-
deselectAll();
|
|
1399
|
-
}
|
|
1395
|
+
deselectAll();
|
|
1400
1396
|
}}
|
|
1401
1397
|
className="Tree-deselector w-full flex-1 p-1 bg-white"
|
|
1402
1398
|
>
|
|
@@ -1426,15 +1422,17 @@ export const Tree = withComponent(
|
|
|
1426
1422
|
withEvents(
|
|
1427
1423
|
withData(
|
|
1428
1424
|
withPermissions(
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1425
|
+
withDropTarget(
|
|
1426
|
+
// withMultiSelection(
|
|
1427
|
+
withSelection(
|
|
1428
|
+
withFilters(
|
|
1429
|
+
withContextMenu(
|
|
1430
|
+
TreeComponent
|
|
1431
|
+
)
|
|
1434
1432
|
)
|
|
1435
1433
|
)
|
|
1436
|
-
)
|
|
1437
|
-
|
|
1434
|
+
// )
|
|
1435
|
+
)
|
|
1438
1436
|
)
|
|
1439
1437
|
)
|
|
1440
1438
|
)
|
|
@@ -1446,20 +1444,22 @@ export const SideTreeEditor = withComponent(
|
|
|
1446
1444
|
withEvents(
|
|
1447
1445
|
withData(
|
|
1448
1446
|
withPermissions(
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1447
|
+
withDropTarget(
|
|
1448
|
+
// withMultiSelection(
|
|
1449
|
+
withSelection(
|
|
1450
|
+
withSideEditor(
|
|
1451
|
+
withFilters(
|
|
1452
|
+
withPresetButtons(
|
|
1453
|
+
withContextMenu(
|
|
1454
|
+
TreeComponent
|
|
1455
|
+
)
|
|
1456
1456
|
)
|
|
1457
|
-
)
|
|
1458
|
-
|
|
1459
|
-
|
|
1457
|
+
),
|
|
1458
|
+
true // isTree
|
|
1459
|
+
)
|
|
1460
1460
|
)
|
|
1461
|
-
)
|
|
1462
|
-
|
|
1461
|
+
// )
|
|
1462
|
+
)
|
|
1463
1463
|
)
|
|
1464
1464
|
)
|
|
1465
1465
|
)
|
|
@@ -1471,20 +1471,22 @@ export const WindowedTreeEditor = withComponent(
|
|
|
1471
1471
|
withEvents(
|
|
1472
1472
|
withData(
|
|
1473
1473
|
withPermissions(
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1474
|
+
withDropTarget(
|
|
1475
|
+
// withMultiSelection(
|
|
1476
|
+
withSelection(
|
|
1477
|
+
withWindowedEditor(
|
|
1478
|
+
withFilters(
|
|
1479
|
+
withPresetButtons(
|
|
1480
|
+
withContextMenu(
|
|
1481
|
+
TreeComponent
|
|
1482
|
+
)
|
|
1481
1483
|
)
|
|
1482
|
-
)
|
|
1483
|
-
|
|
1484
|
-
|
|
1484
|
+
),
|
|
1485
|
+
true // isTree
|
|
1486
|
+
)
|
|
1485
1487
|
)
|
|
1486
|
-
)
|
|
1487
|
-
|
|
1488
|
+
// )
|
|
1489
|
+
)
|
|
1488
1490
|
)
|
|
1489
1491
|
)
|
|
1490
1492
|
)
|