Skip to content

Commit 88bd5bc

Browse files
feat: Enable DefaultEdge as drop target (#278)
1 parent ce455dc commit 88bd5bc

File tree

5 files changed

+274
-9
lines changed

5 files changed

+274
-9
lines changed

packages/demo-app-ts/src/demos/DragDrop.tsx

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
useModel,
1515
useComponentFactory,
1616
ComponentFactory,
17-
GraphElement
17+
GraphElement,
18+
DefaultEdge,
19+
Edge,
20+
isEdge
1821
} from '@patternfly/react-topology';
1922
import defaultComponentFactory from '../components/defaultComponentFactory';
2023
import DemoDefaultNode from '../components/DemoDefaultNode';
@@ -254,6 +257,166 @@ export const DndShiftRegroup = withTopologySetup(() => {
254257
return null;
255258
});
256259

260+
export const DndEdges = withTopologySetup(() => {
261+
useComponentFactory(defaultComponentFactory);
262+
// support pan zoom and drag
263+
useComponentFactory(
264+
React.useCallback<ComponentFactory>((kind, type) => {
265+
if (kind === ModelKind.graph) {
266+
return withPanZoom()(GraphComponent);
267+
}
268+
if (type === 'group-drop') {
269+
return withDndDrop<
270+
GraphElement,
271+
any,
272+
{ droppable?: boolean; hover?: boolean; canDrop?: boolean },
273+
ElementProps
274+
>({
275+
accept: 'test',
276+
canDrop: (item, monitor, props) => !!props && (item as Node).getParent() !== props.element,
277+
collect: (monitor) => ({
278+
droppable: monitor.isDragging(),
279+
hover: monitor.isOver(),
280+
canDrop: monitor.canDrop()
281+
})
282+
})(GroupHull);
283+
}
284+
if (type === 'node-drag') {
285+
return withDndDrag<DragObjectWithType, Node, {}, ElementProps>({
286+
item: { type: 'test' },
287+
begin: (monitor, props) => {
288+
props.element.raise();
289+
return props.element;
290+
},
291+
drag: (event, monitor, props) => {
292+
const nodeElement = props.element as Node;
293+
nodeElement.setPosition(nodeElement.getPosition().clone().translate(event.dx, event.dy));
294+
},
295+
end: (dropResult, monitor, props) => {
296+
if (monitor.didDrop() && dropResult && props) {
297+
if (isEdge(dropResult)) {
298+
const droppedEdge = dropResult as Edge;
299+
const newEdge1 = {
300+
type: droppedEdge.getType(),
301+
id: `${droppedEdge.getSource().getId()}-${props.element.getId()}`,
302+
source: droppedEdge.getSource().getId(),
303+
target: props.element.getId()
304+
};
305+
const newEdge2 = {
306+
type: droppedEdge.getType(),
307+
id: `${props.element.getId()}-${droppedEdge.getTarget().getId()}`,
308+
source: props.element.getId(),
309+
target: droppedEdge.getTarget().getId()
310+
};
311+
const model = droppedEdge.getController().toModel();
312+
const updateModel: Model = { edges: [newEdge1, newEdge2], nodes: model.nodes };
313+
droppedEdge.getController().fromModel(updateModel, true);
314+
} else {
315+
dropResult.appendChild(props.element);
316+
}
317+
}
318+
}
319+
})(DemoDefaultNode);
320+
}
321+
if (type === 'edge') {
322+
return withDndDrop<Node, any, { droppable?: boolean; hover?: boolean; canDrop?: boolean }, ElementProps>({
323+
accept: 'test',
324+
canDrop: (item, monitor, props) =>
325+
!!props &&
326+
(props.element as Edge).getSource().getId() !== item.getId() &&
327+
(props.element as Edge).getTarget().getId() !== item.getId(),
328+
collect: (monitor) => ({
329+
droppable: monitor.isDragging(),
330+
dropTarget: monitor.isOver(),
331+
canDrop: monitor.canDrop()
332+
})
333+
})(DefaultEdge);
334+
}
335+
return undefined;
336+
}, [])
337+
);
338+
useModel(
339+
React.useMemo(
340+
(): Model => ({
341+
graph: {
342+
id: 'g1',
343+
type: 'graph'
344+
},
345+
nodes: [
346+
{
347+
id: 'gr1',
348+
type: 'group-drop',
349+
group: true,
350+
children: ['n2', 'n3'],
351+
style: {
352+
padding: 10
353+
}
354+
},
355+
{
356+
id: 'gr2',
357+
type: 'group-drop',
358+
group: true,
359+
children: ['n4', 'n5'],
360+
style: {
361+
padding: 10
362+
}
363+
},
364+
{
365+
id: 'n1',
366+
type: 'node-drag',
367+
x: 50,
368+
y: 50,
369+
width: 30,
370+
height: 30
371+
},
372+
{
373+
id: 'n2',
374+
type: 'node',
375+
x: 200,
376+
y: 20,
377+
width: 10,
378+
height: 10
379+
},
380+
{
381+
id: 'n3',
382+
type: 'node',
383+
x: 150,
384+
y: 100,
385+
width: 20,
386+
height: 20
387+
},
388+
{
389+
id: 'n4',
390+
type: 'node',
391+
x: 300,
392+
y: 250,
393+
width: 30,
394+
height: 30
395+
},
396+
{
397+
id: 'n5',
398+
type: 'node',
399+
x: 350,
400+
y: 370,
401+
width: 15,
402+
height: 15
403+
}
404+
],
405+
edges: [
406+
{
407+
id: 'e1',
408+
type: 'edge',
409+
source: 'n2',
410+
target: 'n4'
411+
}
412+
]
413+
}),
414+
[]
415+
)
416+
);
417+
return null;
418+
});
419+
257420
export const DragAndDrop: React.FunctionComponent = () => {
258421
const [activeKey, setActiveKey] = React.useState<string | number>(0);
259422

@@ -270,6 +433,9 @@ export const DragAndDrop: React.FunctionComponent = () => {
270433
<Tab eventKey={1} title={<TabTitleText>Dnd Shift Regroup</TabTitleText>}>
271434
<DndShiftRegroup />
272435
</Tab>
436+
<Tab eventKey={2} title={<TabTitleText>Dnd Edges</TabTitleText>}>
437+
<DndEdges />
438+
</Tab>
273439
</Tabs>
274440
</div>
275441
);

packages/demo-app-ts/src/demos/topologyPackageDemo/demoComponentFactory.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ import {
2525
groupDropTargetSpec,
2626
graphDropTargetSpec,
2727
NODE_DRAG_TYPE,
28-
CREATE_CONNECTOR_DROP_TYPE
28+
CREATE_CONNECTOR_DROP_TYPE,
29+
edgeDropTargetSpec,
30+
DragSourceSpec,
31+
DragSpecOperationType,
32+
EditableDragOperationType,
33+
GraphElementProps,
34+
isEdge,
35+
Model
2936
} from '@patternfly/react-topology';
3037
import CustomPathNode from '../../components/CustomPathNode';
3138
import CustomPolygonNode from '../../components/CustomPolygonNode';
@@ -40,6 +47,55 @@ interface EdgeProps {
4047
element: Edge;
4148
}
4249

50+
/* Extend the util 'nodeDragSourceSpec' to also handle dropping a node on an edge to split the edge */
51+
const nodeDragSourceEdgeExtensionSpec = (
52+
type: string,
53+
allowRegroup: boolean = true,
54+
canEdit: boolean = false
55+
): DragSourceSpec<
56+
DragObjectWithType,
57+
DragSpecOperationType<EditableDragOperationType>,
58+
GraphElement,
59+
{
60+
dragging?: boolean;
61+
regrouping?: boolean;
62+
},
63+
GraphElementProps & { canEdit?: boolean }
64+
> => {
65+
const standardSpec = nodeDragSourceSpec(type, allowRegroup, canEdit);
66+
return {
67+
...standardSpec,
68+
end: async (dropResult, monitor, props) => {
69+
if (!monitor.isCancelled() && isEdge(dropResult) && props && monitor.didDrop()) {
70+
const droppedEdge = dropResult as Edge;
71+
const model = droppedEdge.getController().toModel();
72+
73+
const newEdge1 = {
74+
type: droppedEdge.getType(),
75+
id: `${droppedEdge.getSource().getId()}--${props.element.getId()}`,
76+
source: droppedEdge.getSource().getId(),
77+
target: props.element.getId(),
78+
data: droppedEdge.getData()
79+
};
80+
const newEdge2 = {
81+
type: droppedEdge.getType(),
82+
id: `${props.element.getId()}--${droppedEdge.getTarget().getId()}`,
83+
source: props.element.getId(),
84+
target: droppedEdge.getTarget().getId(),
85+
data: droppedEdge.getData()
86+
};
87+
const updateModel: Model = {
88+
edges: [...model.edges.filter((e) => e.id !== droppedEdge.getId()), newEdge1, newEdge2],
89+
nodes: model.nodes
90+
};
91+
droppedEdge.getController().fromModel(updateModel, true);
92+
return undefined;
93+
}
94+
return standardSpec.end(dropResult, monitor, props);
95+
}
96+
};
97+
};
98+
4399
const contextMenuItem = (label: string, i: number): React.ReactElement => {
44100
if (label === '-') {
45101
return <ContextMenuSeparator component="li" key={`separator:${i.toString()}`} />;
@@ -89,7 +145,7 @@ const demoComponentFactory: ComponentFactory = (
89145
})(
90146
withDndDrop(nodeDropTargetSpec([CONNECTOR_SOURCE_DROP, CONNECTOR_TARGET_DROP, CREATE_CONNECTOR_DROP_TYPE]))(
91147
withContextMenu(() => defaultMenu)(
92-
withDragNode(nodeDragSourceSpec('node', true, true))(withSelection()(DemoNode))
148+
withDragNode(nodeDragSourceEdgeExtensionSpec('node', true, true))(withSelection()(DemoNode))
93149
)
94150
)
95151
);
@@ -136,7 +192,11 @@ const demoComponentFactory: ComponentFactory = (
136192
collect: (monitor) => ({
137193
dragging: monitor.isDragging()
138194
})
139-
})(withContextMenu(() => defaultMenu)(withSelection()(DemoEdge)))
195+
})(
196+
withDndDrop<Node, any, { droppable?: boolean; hover?: boolean; canDrop?: boolean }, EdgeProps>(
197+
edgeDropTargetSpec
198+
)(withContextMenu(() => defaultMenu)(withSelection()(DemoEdge)))
199+
)
140200
);
141201
default:
142202
return undefined;

packages/module/src/components/edges/DefaultEdge.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
NodeStatus,
1111
ScaleDetailsLevel
1212
} from '../../types';
13-
import { ConnectDragSource, OnSelect } from '../../behavior';
14-
import { getClosestVisibleParent, useHover } from '../../utils';
13+
import { ConnectDragSource, ConnectDropTarget, OnSelect } from '../../behavior';
14+
import { getClosestVisibleParent, useCombineRefs, useHover } from '../../utils';
1515
import { Layer } from '../layers';
1616
import { css } from '@patternfly/react-styles';
1717
import styles from '../../css/topology-components';
@@ -65,6 +65,12 @@ interface DefaultEdgeProps {
6565
sourceDragRef?: ConnectDragSource;
6666
/** Ref to use to start the drag of the end of the edge. Part of WithTargetDragProps */
6767
targetDragRef?: ConnectDragSource;
68+
/** A ref to add to the node for dropping. Part of WithDndDropProps */
69+
dndDropRef?: ConnectDropTarget;
70+
/** Flag if the current drag operation can be dropped on the edge */
71+
canDrop?: boolean;
72+
/** Flag if the node is the current drop target */
73+
dropTarget?: boolean;
6874
/** Flag indicating if the element is selected. Part of WithSelectionProps */
6975
selected?: boolean;
7076
/** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */
@@ -83,6 +89,9 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
8389
dragging,
8490
sourceDragRef,
8591
targetDragRef,
92+
dndDropRef,
93+
canDrop,
94+
dropTarget,
8695
edgeStyle,
8796
animationDuration,
8897
onShowRemoveConnector,
@@ -105,10 +114,10 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
105114
onContextMenu
106115
}) => {
107116
const [hover, hoverRef] = useHover();
117+
const edgeRef = useCombineRefs(hoverRef, dndDropRef);
108118
const startPoint = element.getStartPoint();
109119
const endPoint = element.getEndPoint();
110120

111-
// eslint-disable-next-line patternfly-react/no-layout-effect
112121
React.useLayoutEffect(() => {
113122
if (hover && !dragging) {
114123
onShowRemoveConnector && onShowRemoveConnector();
@@ -136,7 +145,12 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
136145
);
137146

138147
const edgeAnimationDuration = animationDuration ?? getEdgeAnimationDuration(element.getEdgeAnimationSpeed());
139-
const linkClassName = css(styles.topologyEdgeLink, getEdgeStyleClassModifier(edgeStyle || element.getEdgeStyle()));
148+
const linkClassName = css(
149+
styles.topologyEdgeLink,
150+
canDrop && 'pf-m-highlight',
151+
canDrop && dropTarget && 'pf-m-drop-target',
152+
getEdgeStyleClassModifier(edgeStyle || element.getEdgeStyle())
153+
);
140154

141155
const bendpoints = element.getBendpoints();
142156

@@ -164,7 +178,7 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
164178
return (
165179
<Layer id={dragging || hover ? TOP_LAYER : undefined}>
166180
<g
167-
ref={hoverRef}
181+
ref={edgeRef}
168182
data-test-id="edge-handler"
169183
className={groupClassName}
170184
onClick={onSelect}

packages/module/src/components/factories/components/componentUtils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,20 @@ const edgeDragSourceSpec = (
278278
})
279279
});
280280

281+
const edgeDropTargetSpec: DropTargetSpec<any, any, { droppable: boolean; dropTarget: boolean; canDrop: boolean }, any> =
282+
{
283+
accept: [NODE_DRAG_TYPE],
284+
canDrop: (item, monitor, props) =>
285+
!!props &&
286+
(props.element as Edge).getSource().getId() !== item.id &&
287+
(props.element as Edge).getTarget().getId() !== item.id,
288+
collect: (monitor) => ({
289+
droppable: monitor.isDragging(),
290+
dropTarget: monitor.isOver(),
291+
canDrop: monitor.canDrop()
292+
})
293+
};
294+
281295
const noDropTargetSpec: DropTargetSpec<GraphElement, any, {}, { element: GraphElement }> = {
282296
accept: [NODE_DRAG_TYPE, EDGE_DRAG_TYPE, CREATE_CONNECTOR_DROP_TYPE],
283297
canDrop: () => false
@@ -291,6 +305,7 @@ export {
291305
graphDropTargetSpec,
292306
groupDropTargetSpec,
293307
edgeDragSourceSpec,
308+
edgeDropTargetSpec,
294309
noDropTargetSpec,
295310
REGROUP_OPERATION,
296311
MOVE_CONNECTOR_DROP_TYPE,

0 commit comments

Comments
 (0)