From 6ca011294e2ef3258604f2a488d3f5710979bbb9 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Wed, 13 Nov 2024 13:48:38 +0530 Subject: [PATCH] Feat(DND): Enable Simple DND --- .../Custom/Group/CustomGroupExpanded.tsx | 15 +- .../Visualization/Custom/Node/CustomNode.scss | 4 + .../Visualization/Custom/Node/CustomNode.tsx | 331 +++++++++++------- .../Custom/Node/PlaceholderNode.scss | 4 + .../Custom/Node/PlaceholderNode.tsx | 126 ++++--- .../Visualization/Custom/_custom.scss | 6 + .../Custom/customComponentUtils.ts | 49 +++ .../visualization/base-visual-entity.ts | 15 + .../flows/abstract-camel-visual-entity.ts | 49 ++- .../camel-error-handler-visual-entity.ts | 12 + .../camel-rest-configuration-visual-entity.ts | 12 + .../visualization/flows/pipe-visual-entity.ts | 24 ++ .../visualization/visualization-node.ts | 12 + 13 files changed, 473 insertions(+), 186 deletions(-) create mode 100644 packages/ui/src/components/Visualization/Custom/customComponentUtils.ts diff --git a/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx b/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx index 735abde34..91f939028 100644 --- a/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx +++ b/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx @@ -12,6 +12,7 @@ import { useAnchor, useHover, useSelection, + withDndDrop, } from '@patternfly/react-topology'; import { FunctionComponent, useContext, useRef } from 'react'; import { AddStepMode, IVisualizationNode, NodeToolbarTrigger } from '../../../../models'; @@ -23,9 +24,10 @@ import { AddStepIcon } from '../Edge/AddStepIcon'; import { TargetAnchor } from '../target-anchor'; import './CustomGroupExpanded.scss'; import { CustomGroupProps } from './Group.models'; +import { customGroupExpandedDropTargetSpec } from '../customComponentUtils'; -export const CustomGroupExpanded: FunctionComponent = observer( - ({ element, onContextMenu, onCollapseToggle }) => { +export const CustomGroupExpandedInner: FunctionComponent = observer( + ({ element, onContextMenu, onCollapseToggle, dndDropRef, droppable }) => { if (!isNode(element)) { throw new Error('CustomGroupExpanded must be used only on Node elements'); } @@ -41,7 +43,7 @@ export const CustomGroupExpanded: FunctionComponent = observer CanvasDefaults.HOVER_DELAY_IN, CanvasDefaults.HOVER_DELAY_OUT, ); - const boxRef = useRef(element.getBounds()); + const boxRef = useRef(null); const shouldShowToolbar = settingsAdapter.getSettings().nodeToolbarTrigger === NodeToolbarTrigger.onHover ? isGHover || isToolbarHover || isSelected @@ -58,7 +60,9 @@ export const CustomGroupExpanded: FunctionComponent = observer return null; } - boxRef.current = element.getBounds(); + if (!droppable || !boxRef.current) { + boxRef.current = element.getBounds(); + } const toolbarWidth = Math.max(CanvasDefaults.STEP_TOOLBAR_WIDTH, boxRef.current.width); const toolbarX = boxRef.current.x + (boxRef.current.width - toolbarWidth) / 2; const toolbarY = boxRef.current.y - CanvasDefaults.STEP_TOOLBAR_HEIGHT; @@ -83,6 +87,7 @@ export const CustomGroupExpanded: FunctionComponent = observer onContextMenu={onContextMenu} > = observer ); }, ); + +export const CustomGroupExpanded = withDndDrop(customGroupExpandedDropTargetSpec)(CustomGroupExpandedInner); diff --git a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss index 56866267a..73eaef541 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss +++ b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss @@ -9,6 +9,10 @@ flex-flow: column nowrap; justify-content: space-around; + &__dropTarget { + @include custom.drop-target; + } + &__image { position: relative; display: flex; diff --git a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx index dc8092876..bbded3312 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx +++ b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx @@ -1,20 +1,32 @@ import { Icon } from '@patternfly/react-core'; import { ArrowDownIcon, ArrowRightIcon, BanIcon, ExclamationCircleIcon } from '@patternfly/react-icons'; -import type { DefaultNode, ElementModel, GraphElement, Node } from '@patternfly/react-topology'; import { AnchorEnd, + DefaultNode, DEFAULT_LAYER, + DragObjectWithType, + DragSourceSpec, + DragSpecOperationType, + EditableDragOperationType, + ElementModel, + GraphElement, + GraphElementProps, + isNode, LabelBadge, Layer, + Node, + observer, Rect, TOP_LAYER, - WithSelectionProps, - isNode, - observer, useAnchor, + useCombineRefs, useHover, + useDragNode, useSelection, withContextMenu, + withDndDrop, + withSelection, + useVisualizationController, } from '@patternfly/react-topology'; import clsx from 'clsx'; import { FunctionComponent, useContext, useRef } from 'react'; @@ -27,146 +39,205 @@ import { NodeContextMenuFn } from '../ContextMenu/NodeContextMenu'; import { AddStepIcon } from '../Edge/AddStepIcon'; import { TargetAnchor } from '../target-anchor'; import './CustomNode.scss'; +import { useEntityContext } from '../../../../hooks/useEntityContext/useEntityContext'; +import { customNodeDropTargetSpec } from '../customComponentUtils'; type DefaultNodeProps = Parameters[0]; -interface CustomNodeProps extends DefaultNodeProps, WithSelectionProps { + +interface CustomNodeProps extends DefaultNodeProps { element: GraphElement; /** Toggle node collapse / expand */ onCollapseToggle?: () => void; } -const CustomNode: FunctionComponent = observer(({ element, onContextMenu, onCollapseToggle }) => { - if (!isNode(element)) { - throw new Error('CustomNode must be used only on Node elements'); - } - - const vizNode: IVisualizationNode | undefined = element.getData()?.vizNode; - const settingsAdapter = useContext(SettingsContext); - const label = vizNode?.getNodeLabel(settingsAdapter.getSettings().nodeLabel); - const isDisabled = !!vizNode?.getComponentSchema()?.definition?.disabled; - const tooltipContent = vizNode?.getTooltipContent(); - const validationText = vizNode?.getNodeValidationText(); - const doesHaveWarnings = !isDisabled && !!validationText; - const [isSelected, onSelect] = useSelection(); - const [isGHover, gHoverRef] = useHover(CanvasDefaults.HOVER_DELAY_IN, CanvasDefaults.HOVER_DELAY_OUT); - const [isToolbarHover, toolbarHoverRef] = useHover( - CanvasDefaults.HOVER_DELAY_IN, - CanvasDefaults.HOVER_DELAY_OUT, - ); - const childCount = element.getAllNodeChildren().length; - const boxRef = useRef(element.getBounds()); - const shouldShowToolbar = - settingsAdapter.getSettings().nodeToolbarTrigger === NodeToolbarTrigger.onHover - ? isGHover || isToolbarHover || isSelected - : isSelected; - const shouldShowAddStep = - shouldShowToolbar && vizNode?.getNodeInteraction().canHaveNextStep && vizNode.getNextNode() === undefined; - const isHorizontal = element.getGraph().getLayout() === LayoutType.DagreHorizontal; - - useAnchor((element: Node) => { - return new TargetAnchor(element); - }, AnchorEnd.both); - - const labelX = (boxRef.current.width - CanvasDefaults.DEFAULT_LABEL_WIDTH) / 2; - const toolbarWidth = CanvasDefaults.STEP_TOOLBAR_WIDTH; - const toolbarX = (boxRef.current.width - toolbarWidth) / 2; - const toolbarY = CanvasDefaults.STEP_TOOLBAR_HEIGHT * -1; - - if (!vizNode) { - return null; - } - - return ( - - - -
-
- {tooltipContent} - - {isDisabled && ( - - - - )} -
-
-
- - = observer( + ({ element, onContextMenu, onCollapseToggle, dndDropRef, hover, droppable, canDrop }) => { + if (!isNode(element)) { + throw new Error('CustomNode must be used only on Node elements'); + } + + const vizNode: IVisualizationNode | undefined = element.getData()?.vizNode; + const entitiesContext = useEntityContext(); + const controller = useVisualizationController(); + const settingsAdapter = useContext(SettingsContext); + const label = vizNode?.getNodeLabel(settingsAdapter.getSettings().nodeLabel); + const isDisabled = !!vizNode?.getComponentSchema()?.definition?.disabled; + const tooltipContent = vizNode?.getTooltipContent(); + const validationText = vizNode?.getNodeValidationText(); + const doesHaveWarnings = !isDisabled && !!validationText; + const [isSelected, onSelect] = useSelection(); + const [isGHover, gHoverRef] = useHover(CanvasDefaults.HOVER_DELAY_IN, CanvasDefaults.HOVER_DELAY_OUT); + const [isToolbarHover, toolbarHoverRef] = useHover( + CanvasDefaults.HOVER_DELAY_IN, + CanvasDefaults.HOVER_DELAY_OUT, + ); + const childCount = element.getAllNodeChildren().length; + const boxRef = useRef(null); + const shouldShowToolbar = + settingsAdapter.getSettings().nodeToolbarTrigger === NodeToolbarTrigger.onHover + ? isGHover || isToolbarHover || isSelected + : isSelected; + const shouldShowAddStep = + shouldShowToolbar && vizNode?.getNodeInteraction().canHaveNextStep && vizNode.getNextNode() === undefined; + const isHorizontal = element.getGraph().getLayout() === LayoutType.DagreHorizontal; + + useAnchor((element: Node) => { + return new TargetAnchor(element); + }, AnchorEnd.both); + + const nodeDragSourceSpec: DragSourceSpec< + DragObjectWithType, + DragSpecOperationType, + GraphElement, + object, + GraphElementProps + > = { + item: { type: '#node#' }, + begin: () => { + const graph = controller.getGraph(); + // Hide all edges when dragging starts + graph.getEdges().forEach((edge) => edge.setVisible(false)); + }, + canDrag: () => { + if (settingsAdapter.getSettings().experimentalFeatures.enableDragAndDrop) { + return element.getData()?.vizNode?.canDragNode(); + } else { + return false; + } + }, + end(dropResult, monitor) { + const graph = controller.getGraph(); + // Show all edges after dropping + graph.getEdges().forEach((edge) => edge.setVisible(true)); + + if (monitor.didDrop() && dropResult) { + const draggedNodePath = element.getData().vizNode.data.path; + dropResult.getData()?.vizNode?.moveNodeTo(draggedNodePath); + entitiesContext.updateEntitiesFromCamelResource(); + } else { + controller.getGraph().layout(); + } + }, + }; + + const [_, dragNodeRef] = useDragNode(nodeDragSourceSpec); + const gCombinedRef = useCombineRefs(gHoverRef, dragNodeRef); + + if (!droppable || !boxRef.current) { + boxRef.current = element.getBounds(); + } + const labelX = (boxRef.current.width - CanvasDefaults.DEFAULT_LABEL_WIDTH) / 2; + const toolbarWidth = CanvasDefaults.STEP_TOOLBAR_WIDTH; + const toolbarX = (boxRef.current.width - toolbarWidth) / 2; + const toolbarY = CanvasDefaults.STEP_TOOLBAR_HEIGHT * -1; + + if (!vizNode) { + return null; + } + + return ( + + -
- {doesHaveWarnings && ( - - - - )} - {label} -
-
- - {shouldShowToolbar && ( - - - - - - )} +
+ {tooltipContent} + + {isDisabled && ( + + + + )} +
+ +
- {shouldShowAddStep && ( - - {isHorizontal ? : } - + {doesHaveWarnings && ( + + + + )} + {label} + - )} - {childCount && } - - - ); -}); + {!droppable && shouldShowToolbar && ( + + + + + + )} + + {!droppable && shouldShowAddStep && ( + + + {isHorizontal ? : } + + + )} + + {childCount && } + + + ); + }, +); -export const CustomNodeWithSelection = withContextMenu(NodeContextMenuFn)(CustomNode); +export const CustomNodeWithSelection = withDndDrop(customNodeDropTargetSpec)( + withSelection()(withContextMenu(NodeContextMenuFn)(CustomNode)), +); diff --git a/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.scss b/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.scss index b68cd71ba..e25c770f0 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.scss +++ b/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.scss @@ -9,6 +9,10 @@ align-items: center; justify-content: center; + &__dropTarget { + @include custom.drop-target; + } + &__image { border: 2px dashed var(--custom-node-BorderColor); border-radius: var(--custom-node-BorderRadius); diff --git a/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.tsx b/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.tsx index 21668b75a..38ff2e6e8 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.tsx +++ b/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.tsx @@ -1,7 +1,16 @@ import { Icon } from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons'; import type { DefaultNode, ElementModel, GraphElement, Node } from '@patternfly/react-topology'; -import { AnchorEnd, DEFAULT_LAYER, Layer, Rect, isNode, observer, useAnchor } from '@patternfly/react-topology'; +import { + AnchorEnd, + DEFAULT_LAYER, + Layer, + Rect, + isNode, + observer, + useAnchor, + withDndDrop, +} from '@patternfly/react-topology'; import { FunctionComponent, useContext, useRef } from 'react'; import { IVisualizationNode } from '../../../../models'; import { SettingsContext } from '../../../../providers'; @@ -10,63 +19,78 @@ import { CanvasNode } from '../../Canvas/canvas.models'; import { useReplaceStep } from '../hooks/replace-step.hook'; import { TargetAnchor } from '../target-anchor'; import './PlaceholderNode.scss'; +import clsx from 'clsx'; +import { placeholderNodeDropTargetSpec } from '../customComponentUtils'; type DefaultNodeProps = Parameters[0]; -interface CustomNodeProps extends DefaultNodeProps { +interface PlaceholderNodeInnerProps extends DefaultNodeProps { element: GraphElement; } -export const PlaceholderNode: FunctionComponent = observer(({ element }) => { - if (!isNode(element)) { - throw new Error('PlaceholderNode must be used only on Node elements'); - } - const vizNode: IVisualizationNode | undefined = element.getData()?.vizNode; - const settingsAdapter = useContext(SettingsContext); - const label = vizNode?.getNodeLabel(settingsAdapter.getSettings().nodeLabel); - const updatedLabel = label === 'placeholder' ? 'Add step' : label; - const tooltipContent = 'Click to add a step'; - const boxRef = useRef(element.getBounds()); - const labelX = (boxRef.current.width - CanvasDefaults.DEFAULT_LABEL_WIDTH) / 2; +const PlaceholderNodeInner: FunctionComponent = observer( + ({ element, dndDropRef, hover, canDrop }) => { + if (!isNode(element)) { + throw new Error('PlaceholderNode must be used only on Node elements'); + } + const vizNode: IVisualizationNode | undefined = element.getData()?.vizNode; + const settingsAdapter = useContext(SettingsContext); + const label = vizNode?.getNodeLabel(settingsAdapter.getSettings().nodeLabel); + const updatedLabel = label === 'placeholder' ? 'Add step' : label; + const tooltipContent = 'Click to add a step'; + const boxRef = useRef(element.getBounds()); + const labelX = (boxRef.current.width - CanvasDefaults.DEFAULT_LABEL_WIDTH) / 2; - useAnchor((element: Node) => { - return new TargetAnchor(element); - }, AnchorEnd.both); + useAnchor((element: Node) => { + return new TargetAnchor(element); + }, AnchorEnd.both); - if (!vizNode) { - return null; - } - const { onReplaceNode } = useReplaceStep(vizNode); + if (!vizNode) { + return null; + } + const { onReplaceNode } = useReplaceStep(vizNode); - return ( - - - -
-
- - - + return ( + + + +
+
+ + + +
-
- + - -
- {updatedLabel} -
-
- - - ); -}); + +
+ {updatedLabel} +
+
+ + + ); + }, +); + +export const PlaceholderNode = withDndDrop(placeholderNodeDropTargetSpec)(PlaceholderNodeInner); diff --git a/packages/ui/src/components/Visualization/Custom/_custom.scss b/packages/ui/src/components/Visualization/Custom/_custom.scss index 8fa04773e..eb5c56572 100644 --- a/packages/ui/src/components/Visualization/Custom/_custom.scss +++ b/packages/ui/src/components/Visualization/Custom/_custom.scss @@ -11,6 +11,7 @@ --custom-node-BorderRadius: 10px; --custom-node-hover-BorderColor: var(--pf-v5-global--primary-color--light-100); --custom-node-Shadow: var(--pf-v5-global--BoxShadow--md); + --custom-node-dropTarget-BorderColor: var(--pf-v5-global--palette--light-green-500); &[data-selected='true'] { --custom-node-BorderColor: var(--pf-v5-global--primary-color--dark-100); @@ -62,3 +63,8 @@ @content; } } + +@mixin drop-target { + border: 3px dashed var(--custom-node-dropTarget-BorderColor); + border-radius: 5px; +} diff --git a/packages/ui/src/components/Visualization/Custom/customComponentUtils.ts b/packages/ui/src/components/Visualization/Custom/customComponentUtils.ts new file mode 100644 index 000000000..971d47cb3 --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/customComponentUtils.ts @@ -0,0 +1,49 @@ +import { DropTargetSpec, GraphElement, GraphElementProps, Node } from '@patternfly/react-topology'; + +const NODE_DRAG_TYPE = '#node#'; + +const placeholderNodeDropTargetSpec: DropTargetSpec = { + accept: ['#node#'], + canDrop: (item) => { + const draggedNode = item as Node; + // Do not allow group drop yet + return !draggedNode.getData().vizNode.data.isGroup; + }, + collect: (monitor) => ({ + droppable: monitor.isDragging(), + hover: monitor.isOver(), + canDrop: monitor.canDrop(), + }), +}; + +const customNodeDropTargetSpec: DropTargetSpec = { + accept: ['#node#'], + canDrop: (item, _monitor, props) => { + const targetNode = props.element; + const draggedNode = item as Node; + + // Ensure that the node is not dropped onto itself + if (draggedNode !== targetNode) { + return targetNode.getData()?.vizNode?.canDropOnNode(); + } + + return false; + }, + collect: (monitor) => ({ + droppable: monitor.isDragging(), + hover: monitor.isOver(), + canDrop: monitor.canDrop(), + }), +}; + +const customGroupExpandedDropTargetSpec: DropTargetSpec = { + accept: ['#node#'], + canDrop: () => { + return false; + }, + collect: (monitor) => ({ + droppable: monitor.isDragging(), + }), +}; + +export { customGroupExpandedDropTargetSpec, customNodeDropTargetSpec, placeholderNodeDropTargetSpec, NODE_DRAG_TYPE }; diff --git a/packages/ui/src/models/visualization/base-visual-entity.ts b/packages/ui/src/models/visualization/base-visual-entity.ts index 675259904..a09cdc13c 100644 --- a/packages/ui/src/models/visualization/base-visual-entity.ts +++ b/packages/ui/src/models/visualization/base-visual-entity.ts @@ -47,6 +47,15 @@ export interface BaseVisualCamelEntity extends BaseCamelEntity { targetProperty?: string; }) => void; + /** Check if the node is draggable */ + canDragNode: (path?: string) => boolean; + + /** Check if the node is droppable */ + canDropOnNode: (path?: string) => boolean; + + /** Switch steps */ + moveNodeTo: (options: { draggedNodePath: string; droppedNodePath?: string }) => void; + /** Remove the step at a given path from the underlying Camel entity */ removeStep: (path?: string) => void; @@ -91,6 +100,12 @@ export interface IVisualizationNode implements Bas } } + canDragNode(path?: string) { + if (!isDefined(path)) return false; + + return path !== 'route.from' && path !== 'template.from'; + } + + canDropOnNode(path?: string) { + return this.canDragNode(path); + } + + /** To Do: combine with addstep() + * Try to re-use insertChildStep() + */ + moveNodeTo(options: { draggedNodePath: string; droppedNodePath?: string }) { + if (options.droppedNodePath === undefined) return; + + const pathArray = options.droppedNodePath.split('.'); + const last = pathArray[pathArray.length - 1]; + const penultimate = pathArray[pathArray.length - 2]; + + const componentPath = options.draggedNodePath.split('.'); + let stepsArray: ProcessorDefinition[]; + + if (!Number.isInteger(Number(last)) && Number.isInteger(Number(penultimate))) { + const componentModel = getValue(this.entityDef, componentPath?.slice(0, -1)); + stepsArray = getArrayProperty(this.entityDef, pathArray.slice(0, -2).join('.')); + + /** Remove the dragged node */ + this.removeStep(options.draggedNodePath); + + /** Add the dragged node before the drop target */ + const desiredStartIndex = last === 'placeholder' ? 0 : Number(penultimate); + stepsArray.splice(desiredStartIndex, 0, componentModel); + } + + if (Number.isInteger(Number(last)) && !Number.isInteger(Number(penultimate))) { + const componentModel = getValue(this.entityDef, componentPath); + stepsArray = getArrayProperty(this.entityDef, pathArray.slice(0, -1).join('.')); + + /** Remove the dragged node */ + this.removeStep(options.draggedNodePath); + + /** Add the dragged node before the drop target */ + stepsArray.splice(Number(last), 0, componentModel); + } + } + removeStep(path?: string): void { if (!path) return; const pathArray = path.split('.'); diff --git a/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts index 5272686e7..323db5c9d 100644 --- a/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts @@ -90,6 +90,18 @@ export class CamelErrorHandlerVisualEntity implements BaseVisualCamelEntity { return; } + canDragNode(_path?: string) { + return false; + } + + canDropOnNode(_path?: string) { + return false; + } + + moveNodeTo(_options: { draggedNodePath: string; droppedNodePath?: string }) { + return; + } + removeStep(): void { return; } diff --git a/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts index f70cd2e53..d9b4fc8ab 100644 --- a/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts @@ -73,6 +73,18 @@ export class CamelRestConfigurationVisualEntity implements BaseVisualCamelEntity return; } + canDragNode(_path?: string) { + return false; + } + + canDropOnNode(_path?: string) { + return false; + } + + moveNodeTo(_options: { draggedNodePath: string; droppedNodePath?: string }) { + return; + } + removeStep(): void { return; } diff --git a/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts b/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts index e45a40910..f117f20b7 100644 --- a/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts @@ -9,6 +9,7 @@ import { updatePipeFromCustomSchema, setValue, getValue, + isDefined, } from '../../../utils'; import { DefinedComponent } from '../../camel-catalog-index'; import { EntityType } from '../../camel/entities'; @@ -167,6 +168,29 @@ export class PipeVisualEntity implements BaseVisualCamelEntity { } } + canDragNode(path?: string) { + if (!isDefined(path)) return false; + + return path !== 'source' && path !== 'sink'; + } + + canDropOnNode(path?: string) { + return this.canDragNode(path); + } + + moveNodeTo(options: { draggedNodePath: string; droppedNodePath?: string }) { + if (options.droppedNodePath === undefined) return; + + const step = getValue(this.pipe.spec!, options.draggedNodePath); + const kameletArray = getArrayProperty(this.pipe.spec!, 'steps'); + + /** Remove the dragged node */ + this.removeStep(options.draggedNodePath); + + /** Add the dragged node at the target node index */ + kameletArray.splice(Number(options.droppedNodePath.split('.').pop()), 0, step); + } + removeStep(path?: string): void { /** This method needs to be enabled after passing the entire parent to this class*/ if (!path) return; diff --git a/packages/ui/src/models/visualization/visualization-node.ts b/packages/ui/src/models/visualization/visualization-node.ts index 02faca59e..bdf2db0e3 100644 --- a/packages/ui/src/models/visualization/visualization-node.ts +++ b/packages/ui/src/models/visualization/visualization-node.ts @@ -54,6 +54,18 @@ class VisualizationNode