Skip to content

Commit cf86175

Browse files
shivamG640lordrip
authored andcommitted
feat(canvas): support drag-and-drop of compatible groups regardless of collapsed or expanded state
1 parent 592017b commit cf86175

7 files changed

Lines changed: 167 additions & 52 deletions

File tree

packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,19 @@
7979
}
8080

8181
&__dropTarget-right {
82-
border-right: var(--custom-group-border-dropTarget);
82+
border-right: var(--custom-component-border-dropTarget);
8383
}
8484

8585
&__dropTarget-left {
86-
border-left: var(--custom-group-border-dropTarget);
86+
border-left: var(--custom-component-border-dropTarget);
8787
}
8888

8989
&__possibleDropTarget-right {
90-
border-right: var(--custom-group-border-possibleDropTarget);
90+
border-right: var(--custom-component-border-possibleDropTarget);
9191
}
9292

9393
&__possibleDropTarget-left {
94-
border-left: var(--custom-group-border-possibleDropTarget);
94+
border-left: var(--custom-component-border-possibleDropTarget);
9595
}
9696
}
9797

packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ import { IconResolver } from '../../../IconResolver';
3939
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';
4040
import { CanvasDefaults } from '../../Canvas/canvas.defaults';
4141
import { StepToolbar } from '../../Canvas/StepToolbar/StepToolbar';
42-
import { canDragGroup, GROUP_DRAG_TYPE } from '../customComponentUtils';
42+
import {
43+
canDragGroup,
44+
getDropTargetContainerClassNames,
45+
GROUP_DRAG_TYPE,
46+
NODE_DRAG_TYPE,
47+
} from '../customComponentUtils';
4348
import { FloatingCircle } from '../FloatingCircle/FloatingCircle';
4449
import { CustomNodeContainer } from '../Node/CustomNodeContainer';
4550
import { checkNodeDropCompatibility, getNodeDragAndDropDirection, handleValidNodeDrop } from '../Node/CustomNodeUtils';
@@ -120,10 +125,10 @@ export const CustomGroupExpandedInner: FunctionComponent<CustomGroupProps> = obs
120125
GraphElementProps
121126
> = useMemo(
122127
() => ({
123-
accept: [GROUP_DRAG_TYPE],
128+
accept: [NODE_DRAG_TYPE, GROUP_DRAG_TYPE],
124129
canDrop: (item, _monitor, _props) => {
125130
// Ensure that the node is not dropped onto itself
126-
if (item === element) return false;
131+
if ((item as Node) === element) return false;
127132

128133
return checkNodeDropCompatibility(
129134
item.getData()?.vizNode,
@@ -161,10 +166,15 @@ export const CustomGroupExpandedInner: FunctionComponent<CustomGroupProps> = obs
161166
element.getData().vizNode.data.path.slice(0, draggedVizNode?.data.path.length) === draggedVizNode?.data.path;
162167
const gCombinedRef = useCombineRefs<SVGGElement>(gHoverRef, dragGroupRef);
163168

164-
let dropDirection: 'forward' | 'backward' | null = null;
165-
if (dndDropProps.droppable && dndDropProps.canDrop && draggedVizNode) {
166-
dropDirection = getNodeDragAndDropDirection(draggedVizNode, groupVizNode, false);
167-
}
169+
const dropDirection: 'forward' | 'backward' | null =
170+
dndDropProps.droppable && dndDropProps.canDrop && draggedVizNode
171+
? getNodeDragAndDropDirection(draggedVizNode, groupVizNode, false)
172+
: null;
173+
174+
const mainContainerClassNames = {
175+
[`custom-group__container__draggedGroup`]: isDraggingGroup || refreshGroup,
176+
...getDropTargetContainerClassNames('custom-group__container', dropDirection, dndDropProps.hover),
177+
};
168178

169179
const box = element.getBounds();
170180
if (!dndDropProps.droppable || !boxRef.current) {
@@ -199,13 +209,7 @@ export const CustomGroupExpandedInner: FunctionComponent<CustomGroupProps> = obs
199209
>
200210
<div
201211
data-testid={`${groupVizNode.getId()}|${groupVizNode.id}`}
202-
className={clsx('custom-group__container', {
203-
'custom-group__container__draggedGroup': isDraggingGroup || refreshGroup,
204-
'custom-group__container__dropTarget-right': dropDirection === 'forward' && dndDropProps.hover,
205-
'custom-group__container__dropTarget-left': dropDirection === 'backward' && dndDropProps.hover,
206-
'custom-group__container__possibleDropTarget-right': dropDirection === 'forward' && !dndDropProps.hover,
207-
'custom-group__container__possibleDropTarget-left': dropDirection === 'backward' && !dndDropProps.hover,
208-
})}
212+
className={clsx('custom-group__container', mainContainerClassNames)}
209213
>
210214
<div className="custom-group__container__text" title={tooltipContent}>
211215
{doesHaveWarnings ? (

packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010
flex-flow: column nowrap;
1111
justify-content: space-around;
1212

13+
&__dropTarget-right {
14+
border-right: var(--custom-component-border-dropTarget);
15+
border-radius: var(--custom-node-BorderRadius);
16+
}
17+
18+
&__dropTarget-left {
19+
border-left: var(--custom-component-border-dropTarget);
20+
border-radius: var(--custom-node-BorderRadius);
21+
}
22+
23+
&__possibleDropTarget-right {
24+
border-right: var(--custom-component-border-possibleDropTarget);
25+
}
26+
27+
&__possibleDropTarget-left {
28+
border-left: var(--custom-component-border-possibleDropTarget);
29+
}
30+
1331
&__draggable {
1432
@include dnd.cursor-grab;
1533
}

packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ import { CanvasDefaults } from '../../Canvas/canvas.defaults';
4141
import { CanvasNode } from '../../Canvas/canvas.models';
4242
import { StepToolbar } from '../../Canvas/StepToolbar/StepToolbar';
4343
import { NodeContextMenuFn } from '../ContextMenu/NodeContextMenu';
44-
import { GROUP_DRAG_TYPE, NODE_DRAG_TYPE } from '../customComponentUtils';
44+
import { getDropTargetContainerClassNames, GROUP_DRAG_TYPE, NODE_DRAG_TYPE } from '../customComponentUtils';
4545
import { TargetAnchor } from '../target-anchor';
4646
import { CustomNodeContainer } from './CustomNodeContainer';
47-
import { checkNodeDropCompatibility, handleValidNodeDrop } from './CustomNodeUtils';
47+
import { checkNodeDropCompatibility, getNodeDragAndDropDirection, handleValidNodeDrop } from './CustomNodeUtils';
4848

4949
type DefaultNodeProps = Parameters<typeof DefaultNode>[0];
5050

@@ -98,6 +98,16 @@ const CustomNodeLabel: FunctionComponent<CustomNodeLabelProps> = ({
9898
</foreignObject>
9999
);
100100

101+
function getShouldShowToolbar(
102+
trigger: NodeToolbarTrigger | undefined,
103+
isGHover: boolean,
104+
isToolbarHover: boolean,
105+
selected: boolean | undefined,
106+
): boolean {
107+
const isHoverTrigger = trigger === NodeToolbarTrigger.onHover;
108+
return isHoverTrigger ? isGHover || isToolbarHover || !!selected : !!selected;
109+
}
110+
101111
const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
102112
({ element, onContextMenu, onCollapseToggle, selected, onSelect }) => {
103113
if (!isNode(element)) {
@@ -125,10 +135,12 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
125135
CanvasDefaults.HOVER_DELAY_OUT,
126136
);
127137
const childCount = element.getAllNodeChildren().length;
128-
const shouldShowToolbar =
129-
settingsAdapter.getSettings().nodeToolbarTrigger === NodeToolbarTrigger.onHover
130-
? isGHover || isToolbarHover || selected
131-
: selected;
138+
const shouldShowToolbar = getShouldShowToolbar(
139+
settingsAdapter.getSettings().nodeToolbarTrigger,
140+
isGHover,
141+
isToolbarHover,
142+
selected,
143+
);
132144
const canDragNode = vizNode?.canDragNode() ?? false;
133145

134146
const hasSomeInteractions = useMemo(
@@ -158,7 +170,7 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
158170
DragObjectWithType,
159171
DragSpecOperationType<EditableDragOperationType>,
160172
GraphElement,
161-
{ node: GraphElement | undefined },
173+
object,
162174
GraphElementProps
163175
> = useMemo(
164176
() => ({
@@ -174,9 +186,6 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
174186
element.getGraph().layout();
175187
}
176188
},
177-
collect: (monitor) => ({
178-
node: monitor.getItem(),
179-
}),
180189
}),
181190
[canDragNode, element, entitiesContext, nodeInteractionAddonContext],
182191
);
@@ -194,7 +203,7 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
194203
GraphElementProps
195204
> = useMemo(
196205
() => ({
197-
accept: [NODE_DRAG_TYPE],
206+
accept: [NODE_DRAG_TYPE, GROUP_DRAG_TYPE],
198207
canDrop: (item, _monitor, _props) => {
199208
const targetNode = element;
200209
const draggedNode = item as Node;
@@ -226,10 +235,10 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
226235
[element, vizNode, entitiesContext, catalogModalContext],
227236
);
228237

229-
const [dragNodeProps, dragNodeRef] = useDragNode(nodeDragSourceSpec);
238+
const [, dragNodeRef] = useDragNode(nodeDragSourceSpec);
230239
const [dndDropProps, dndDropRef] = useDndDrop(customNodeDropTargetSpec);
231240
const gCombinedRef = useCombineRefs<SVGGElement>(gHoverRef, dragNodeRef);
232-
const isDraggingNode = dragNodeProps.node?.getId() === element.getId();
241+
const isDraggingNode = dndDropProps.dragItem?.getId() === element.getId();
233242
const isDraggingNodeType = dndDropProps.dragItemType === NODE_DRAG_TYPE;
234243
const isDraggingGroupType = dndDropProps.dragItemType === GROUP_DRAG_TYPE;
235244
const draggedVizNode = dndDropProps.dragItem?.getData().vizNode;
@@ -249,8 +258,25 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
249258
const toolbarX = (box.width - toolbarWidth) / 2;
250259
const toolbarY = CanvasDefaults.STEP_TOOLBAR_HEIGHT * -1;
251260

261+
const dropDirection: 'forward' | 'backward' | null =
262+
dndDropProps.droppable && dndDropProps.canDrop && draggedVizNode
263+
? getNodeDragAndDropDirection(draggedVizNode, vizNode, false)
264+
: null;
265+
266+
const showMainNodeContainer = !dndDropProps.droppable || isDraggingNodeType || !isDraggingWithinGroup;
267+
const showLabelWhenDragging =
268+
(isDraggingNodeType && !isDraggingNode) || (isDraggingGroupType && !isDraggingWithinGroup);
269+
const showToolbarSection = !dndDropProps.droppable && shouldShowToolbar && hasSomeInteractions;
270+
const layerId = isDraggingWithinGroup ? TOP_LAYER : DEFAULT_LAYER;
271+
272+
const mainContainerClassNames = {
273+
...getDropTargetContainerClassNames('custom-node__container', dropDirection, dndDropProps.hover),
274+
['custom-node__container__draggable']: canDragNode,
275+
['custom-node__container__draggedNode']: isDraggedNode,
276+
};
277+
252278
return (
253-
<Layer id={isDraggingWithinGroup ? TOP_LAYER : DEFAULT_LAYER} data-lastupdate={lastUpdate}>
279+
<Layer id={layerId} data-lastupdate={lastUpdate}>
254280
<g
255281
ref={gCombinedRef}
256282
className="custom-node"
@@ -266,21 +292,14 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
266292
{/** The original node (appears when nothing is dragging, it also acts as the dragged node when node drag action is performed.
267293
* When a group/container is being dragged, the within-group nodes are hidden but the rest of the nodes show this original node.
268294
*/}
269-
{(!dndDropProps.droppable || isDraggingNodeType || !isDraggingWithinGroup) && (
295+
{showMainNodeContainer && (
270296
<CustomNodeContainer
271297
width={box.width}
272298
height={box.height}
273299
dataNodelabel={label}
274-
foreignObjectRef={dndDropRef}
300+
foreignObjectRef={isDraggingNode ? null : dndDropRef}
275301
dataTestId={vizNode.id}
276-
containerClassNames={{
277-
'custom-node__container__dropTarget':
278-
dndDropProps.droppable && dndDropProps.canDrop && dndDropProps.hover,
279-
'custom-node__container__possibleDropTargets':
280-
dndDropProps.canDrop && dndDropProps.droppable && !dndDropProps.hover,
281-
'custom-node__container__draggable': canDragNode,
282-
'custom-node__container__draggedNode': isDraggedNode,
283-
}}
302+
containerClassNames={mainContainerClassNames}
284303
vizNode={vizNode}
285304
tooltipContent={tooltipContent}
286305
childCount={childCount}
@@ -298,9 +317,7 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
298317
dataNodelabel={label}
299318
transform={`translate(${boxXRef.current - box.x}, ${boxYRef.current - box.y})`}
300319
dataTestId={`${vizNode.id}-dummy`}
301-
containerClassNames={{
302-
'custom-node__container__draggedNode': isDraggedNode,
303-
}}
320+
containerClassNames={{ 'custom-node__container__draggedNode': isDraggedNode }}
304321
vizNode={vizNode}
305322
tooltipContent={tooltipContent}
306323
childCount={childCount}
@@ -309,8 +326,9 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
309326
isDisabled={isDisabled}
310327
/>
311328
)}
329+
312330
{/** This label, appears for the node which are not dragging */}
313-
{label && ((isDraggingNodeType && !isDraggingNode) || (isDraggingGroupType && !isDraggingWithinGroup)) && (
331+
{label && showLabelWhenDragging && (
314332
<CustomNodeLabel
315333
label={label}
316334
doesHaveWarnings={doesHaveWarnings}
@@ -341,7 +359,7 @@ const CustomNodeInner: FunctionComponent<CustomNodeProps> = observer(
341359
/>
342360
)}
343361

344-
{!dndDropProps.droppable && shouldShowToolbar && hasSomeInteractions && (
362+
{showToolbarSection && (
345363
<Layer id={TOP_LAYER}>
346364
<foreignObject
347365
ref={toolbarHoverRef}

packages/ui/src/components/Visualization/Custom/_custom.scss

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
--custom-node-BorderColor-hover: var(--pf-t--global--border--color--hover);
1818
--custom-component-dropTarget: var(--pf-t--global--color--status--success--default);
1919
--custom-component-possibleDropTarget: var(--pf-t--global--color--brand--100);
20-
--custom-group-border-dropTarget: 8px solid var(--custom-component-dropTarget);
21-
--custom-group-border-possibleDropTarget: 8px dashed var(--custom-component-possibleDropTarget);
22-
--custom-node-BorderColor-possibleDropTargets: var(--pf-t--global--border--color--default);
20+
--custom-component-border-dropTarget: 8px solid var(--custom-component-dropTarget);
21+
--custom-component-border-possibleDropTarget: 8px dashed var(--custom-component-possibleDropTarget);
2322
--custom-node-BackgroundColor: var(--pf-t--global--background--color--primary--default);
2423
--custom-node-BorderRadius: 10px;
2524
--custom-node-Shadow: var(--pf-t--global--box-shadow--md);

packages/ui/src/components/Visualization/Custom/customComponentUtils.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,70 @@ import { AddStepMode, IVisualizationNode } from '../../../models/visualization/b
55
import { CamelComponentSchemaService } from '../../../models/visualization/flows/support/camel-component-schema.service';
66
import { CamelRouteVisualEntityData } from '../../../models/visualization/flows/support/camel-component-types';
77
import { EntitiesContextResult } from '../../../providers';
8-
import { canDragGroup, canDropOnEdge } from './customComponentUtils';
8+
import { canDragGroup, canDropOnEdge, getDropTargetContainerClassNames } from './customComponentUtils';
9+
10+
describe('getDropTargetContainerClassNames', () => {
11+
const prefix = 'custom-node__container';
12+
13+
it('returns dropTarget-right true when direction is forward and hover is true', () => {
14+
const result = getDropTargetContainerClassNames(prefix, 'forward', true);
15+
16+
expect(Object.keys(result)).toHaveLength(4);
17+
expect(result[`${prefix}__dropTarget-right`]).toBe(true);
18+
expect(result[`${prefix}__dropTarget-left`]).toBe(false);
19+
expect(result[`${prefix}__possibleDropTarget-right`]).toBe(false);
20+
expect(result[`${prefix}__possibleDropTarget-left`]).toBe(false);
21+
});
22+
23+
it('returns possibleDropTarget-right true when direction is forward and hover is false', () => {
24+
const result = getDropTargetContainerClassNames(prefix, 'forward', false);
25+
26+
expect(Object.keys(result)).toHaveLength(4);
27+
expect(result[`${prefix}__dropTarget-right`]).toBe(false);
28+
expect(result[`${prefix}__dropTarget-left`]).toBe(false);
29+
expect(result[`${prefix}__possibleDropTarget-right`]).toBe(true);
30+
expect(result[`${prefix}__possibleDropTarget-left`]).toBe(false);
31+
});
32+
33+
it('returns dropTarget-left true when direction is backward and hover is true', () => {
34+
const result = getDropTargetContainerClassNames(prefix, 'backward', true);
35+
36+
expect(Object.keys(result)).toHaveLength(4);
37+
expect(result[`${prefix}__dropTarget-right`]).toBe(false);
38+
expect(result[`${prefix}__dropTarget-left`]).toBe(true);
39+
expect(result[`${prefix}__possibleDropTarget-right`]).toBe(false);
40+
expect(result[`${prefix}__possibleDropTarget-left`]).toBe(false);
41+
});
42+
43+
it('returns possibleDropTarget-left true when direction is backward and hover is false', () => {
44+
const result = getDropTargetContainerClassNames(prefix, 'backward', false);
45+
46+
expect(Object.keys(result)).toHaveLength(4);
47+
expect(result[`${prefix}__dropTarget-right`]).toBe(false);
48+
expect(result[`${prefix}__dropTarget-left`]).toBe(false);
49+
expect(result[`${prefix}__possibleDropTarget-right`]).toBe(false);
50+
expect(result[`${prefix}__possibleDropTarget-left`]).toBe(true);
51+
});
52+
53+
it('returns all drop-target flags false when dropDirection is null', () => {
54+
const result = getDropTargetContainerClassNames(prefix, null, true);
55+
56+
expect(Object.keys(result)).toHaveLength(4);
57+
expect(result[`${prefix}__dropTarget-right`]).toBe(false);
58+
expect(result[`${prefix}__dropTarget-left`]).toBe(false);
59+
expect(result[`${prefix}__possibleDropTarget-right`]).toBe(false);
60+
expect(result[`${prefix}__possibleDropTarget-left`]).toBe(false);
61+
});
62+
63+
it('uses a custom prefix for class names', () => {
64+
const customPrefix = 'custom-group__container__';
65+
const result = getDropTargetContainerClassNames(customPrefix, 'forward', true);
66+
67+
expect(Object.keys(result)).toHaveLength(4);
68+
expect(result[`${customPrefix}__dropTarget-right`]).toBe(true);
69+
expect(result).not.toHaveProperty(`${prefix}__dropTarget-right`);
70+
});
71+
});
972

1073
describe('canDropOnEdge', () => {
1174
const getMockVizNode = (id: string): IVisualizationNode => {

packages/ui/src/components/Visualization/Custom/customComponentUtils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ import { EntitiesContextResult } from '../../../providers';
77
const NODE_DRAG_TYPE = '#node#';
88
const GROUP_DRAG_TYPE = '#group#';
99

10+
type DropDirection = 'forward' | 'backward' | null;
11+
12+
const getDropTargetContainerClassNames = (
13+
prefix: string,
14+
dropDirection: DropDirection,
15+
hover: boolean,
16+
): Record<string, boolean> => ({
17+
[`${prefix}__dropTarget-right`]: dropDirection === 'forward' && hover,
18+
[`${prefix}__dropTarget-left`]: dropDirection === 'backward' && hover,
19+
[`${prefix}__possibleDropTarget-right`]: dropDirection === 'forward' && !hover,
20+
[`${prefix}__possibleDropTarget-left`]: dropDirection === 'backward' && !hover,
21+
});
22+
1023
const canDropOnEdge = (
1124
draggedVizNode: IVisualizationNode,
1225
potentialEdge: Edge<EdgeModel, unknown>,
@@ -59,4 +72,4 @@ const canDragGroup = (groupVizNode?: IVisualizationNode): boolean => {
5972
return true;
6073
};
6174

62-
export { canDragGroup, canDropOnEdge, GROUP_DRAG_TYPE, NODE_DRAG_TYPE };
75+
export { canDragGroup, canDropOnEdge, getDropTargetContainerClassNames, GROUP_DRAG_TYPE, NODE_DRAG_TYPE };

0 commit comments

Comments
 (0)