From 98e4a92e2a8c9179868dbe34c054dd7a4ab3f005 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 --- .../Canvas/controller.service.ts | 4 +- .../Custom/Group/CustomGroupCollapsible.tsx | 4 +- .../Custom/Group/CustomGroupExpanded.tsx | 25 +- .../Custom/Group/Group.models.ts | 10 +- .../Visualization/Custom/Node/CustomNode.scss | 5 + .../Visualization/Custom/Node/CustomNode.tsx | 356 +++++++++++------- .../Custom/Node/PlaceholderNode.scss | 5 + .../Custom/Node/PlaceholderNode.tsx | 151 +++++--- .../Visualization/Custom/_custom.scss | 1 + .../visualization/base-visual-entity.ts | 12 + .../flows/abstract-camel-visual-entity.ts | 42 +++ .../camel-error-handler-visual-entity.ts | 8 + .../camel-rest-configuration-visual-entity.ts | 8 + .../visualization/flows/pipe-visual-entity.ts | 21 ++ .../visualization/visualization-node.ts | 12 + 15 files changed, 473 insertions(+), 191 deletions(-) diff --git a/packages/ui/src/components/Visualization/Canvas/controller.service.ts b/packages/ui/src/components/Visualization/Canvas/controller.service.ts index 7b249f0f8..dcc331134 100644 --- a/packages/ui/src/components/Visualization/Canvas/controller.service.ts +++ b/packages/ui/src/components/Visualization/Canvas/controller.service.ts @@ -12,7 +12,7 @@ import { withPanZoom, } from '@patternfly/react-topology'; import { CustomGroupWithSelection, CustomNodeWithSelection, NoBendpointsEdge } from '../Custom'; -import { PlaceholderNode } from '../Custom/Node/PlaceholderNode'; +import { PlaceholderNodeDroppable } from '../Custom/Node/PlaceholderNode'; import { LayoutType } from './canvas.models'; import { CustomEdge } from '../Custom/Edge/CustomEdge'; @@ -52,7 +52,7 @@ export class ControllerService { case 'group': return CustomGroupWithSelection; case 'node-placeholder': - return PlaceholderNode; + return PlaceholderNodeDroppable; default: switch (kind) { case ModelKind.graph: diff --git a/packages/ui/src/components/Visualization/Custom/Group/CustomGroupCollapsible.tsx b/packages/ui/src/components/Visualization/Custom/Group/CustomGroupCollapsible.tsx index b5e78d6c6..745fa1cd9 100644 --- a/packages/ui/src/components/Visualization/Custom/Group/CustomGroupCollapsible.tsx +++ b/packages/ui/src/components/Visualization/Custom/Group/CustomGroupCollapsible.tsx @@ -2,7 +2,7 @@ import { isNode, observer } from '@patternfly/react-topology'; import { FunctionComponent } from 'react'; import { useCollapseStep } from '../hooks/collapse-step.hook'; import { CustomNodeWithSelection } from '../Node/CustomNode'; -import { CustomGroupExpanded } from './CustomGroupExpanded'; +import { CustomGroupExpendedWithDndDrop } from './CustomGroupExpanded'; import { CustomGroupProps } from './Group.models'; export const CustomGroupCollapsible: FunctionComponent = observer( @@ -26,7 +26,7 @@ export const CustomGroupCollapsible: FunctionComponent = obser } return ( - { diff --git a/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx b/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx index 735abde34..06d0efc3d 100644 --- a/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx +++ b/packages/ui/src/components/Visualization/Custom/Group/CustomGroupExpanded.tsx @@ -2,7 +2,10 @@ import { Icon } from '@patternfly/react-core'; import { ArrowDownIcon, ArrowRightIcon, BanIcon } from '@patternfly/react-icons'; import { AnchorEnd, + DropTargetSpec, GROUPS_LAYER, + GraphElement, + GraphElementProps, Layer, Node, Rect, @@ -12,6 +15,7 @@ import { useAnchor, useHover, useSelection, + withDndDrop, } from '@patternfly/react-topology'; import { FunctionComponent, useContext, useRef } from 'react'; import { AddStepMode, IVisualizationNode, NodeToolbarTrigger } from '../../../../models'; @@ -25,7 +29,7 @@ import './CustomGroupExpanded.scss'; import { CustomGroupProps } from './Group.models'; export const CustomGroupExpanded: FunctionComponent = observer( - ({ element, onContextMenu, onCollapseToggle }) => { + ({ element, onContextMenu, onCollapseToggle, dndDropRef, droppable }) => { if (!isNode(element)) { throw new Error('CustomGroupExpanded must be used only on Node elements'); } @@ -41,7 +45,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 +62,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 +89,7 @@ export const CustomGroupExpanded: FunctionComponent = observer onContextMenu={onContextMenu} > = observer ); }, ); + +const groupDropTargetSpec: DropTargetSpec = { + accept: ['#node#'], + canDrop: () => { + return false; + }, + collect: (monitor) => ({ + droppable: monitor.isDragging(), + }), +}; + +export const CustomGroupExpendedWithDndDrop = withDndDrop(groupDropTargetSpec)(CustomGroupExpanded); diff --git a/packages/ui/src/components/Visualization/Custom/Group/Group.models.ts b/packages/ui/src/components/Visualization/Custom/Group/Group.models.ts index 4b4d715c5..0398e10b0 100644 --- a/packages/ui/src/components/Visualization/Custom/Group/Group.models.ts +++ b/packages/ui/src/components/Visualization/Custom/Group/Group.models.ts @@ -1,9 +1,15 @@ -import type { ElementModel, GraphElement, DefaultGroup as TopologyDefaultGroup } from '@patternfly/react-topology'; +import type { + ElementModel, + GraphElement, + DefaultGroup as TopologyDefaultGroup, + WithDndDropProps, +} from '@patternfly/react-topology'; import { CanvasNode } from '../../Canvas'; type DefaultGroupProps = Parameters[0]; -export interface CustomGroupProps extends DefaultGroupProps { +export interface CustomGroupProps extends DefaultGroupProps, WithDndDropProps { element: GraphElement; /** Toggle node collapse / expand */ onCollapseToggle?: () => void; + droppable?: boolean; } diff --git a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss index 56866267a..93c456e4b 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss +++ b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.scss @@ -9,6 +9,11 @@ flex-flow: column nowrap; justify-content: space-around; + &__dropTarget { + border: 3px dashed var(--custom-node-dropTarget-BorderColor); + border-radius: 5px; + } + &__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..6697259ad 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx +++ b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx @@ -1,20 +1,35 @@ 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, + DropTargetSpec, + EditableDragOperationType, + ElementModel, + GraphElement, + GraphElementProps, + isNode, LabelBadge, Layer, + Node, + observer, Rect, TOP_LAYER, - WithSelectionProps, - isNode, - observer, useAnchor, + useCombineRefs, useHover, + useDragNode, useSelection, withContextMenu, + withDndDrop, + withSelection, + WithSelectionProps, + WithDndDropProps, + useVisualizationController, } from '@patternfly/react-topology'; import clsx from 'clsx'; import { FunctionComponent, useContext, useRef } from 'react'; @@ -27,146 +42,227 @@ import { NodeContextMenuFn } from '../ContextMenu/NodeContextMenu'; import { AddStepIcon } from '../Edge/AddStepIcon'; import { TargetAnchor } from '../target-anchor'; import './CustomNode.scss'; +import { useEntityContext } from '../../../../hooks/useEntityContext/useEntityContext'; type DefaultNodeProps = Parameters[0]; -interface CustomNodeProps extends DefaultNodeProps, WithSelectionProps { + +interface CustomNodeProps extends DefaultNodeProps, WithSelectionProps, WithDndDropProps { element: GraphElement; /** Toggle node collapse / expand */ onCollapseToggle?: () => void; + hover?: boolean; + droppable?: boolean; + canDrop?: boolean; } -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?.switchSteps(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 && } + + + ); + }, +); + +const nodeDropTargetSpec: 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(), + }), +}; -export const CustomNodeWithSelection = withContextMenu(NodeContextMenuFn)(CustomNode); +export const CustomNodeWithSelection = withDndDrop(nodeDropTargetSpec)( + 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..ef5fbb8f6 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.scss +++ b/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.scss @@ -9,6 +9,11 @@ align-items: center; justify-content: center; + &__dropTarget { + border: 3px dashed var(--custom-node-dropTarget-BorderColor); + border-radius: 5px; + } + &__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..7c6014d73 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.tsx +++ b/packages/ui/src/components/Visualization/Custom/Node/PlaceholderNode.tsx @@ -1,7 +1,24 @@ 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 type { + DefaultNode, + DropTargetSpec, + ElementModel, + GraphElement, + GraphElementProps, + Node, + WithDndDropProps, +} 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 +27,93 @@ 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'; type DefaultNodeProps = Parameters[0]; -interface CustomNodeProps extends DefaultNodeProps { +interface PlaceholderNodeProps extends DefaultNodeProps, WithDndDropProps { element: GraphElement; + hover?: boolean; + canDrop?: boolean; } -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; +export const PlaceholderNode: 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} +
+
+ + + ); + }, +); + +const nodeDropTargetSpec: 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(), + }), +}; + +export const PlaceholderNodeDroppable = withDndDrop(nodeDropTargetSpec)(PlaceholderNode); diff --git a/packages/ui/src/components/Visualization/Custom/_custom.scss b/packages/ui/src/components/Visualization/Custom/_custom.scss index 8fa04773e..22f697090 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); diff --git a/packages/ui/src/models/visualization/base-visual-entity.ts b/packages/ui/src/models/visualization/base-visual-entity.ts index 675259904..4568fda63 100644 --- a/packages/ui/src/models/visualization/base-visual-entity.ts +++ b/packages/ui/src/models/visualization/base-visual-entity.ts @@ -47,6 +47,12 @@ export interface BaseVisualCamelEntity extends BaseCamelEntity { targetProperty?: string; }) => void; + /** Check if the node is draggable */ + isDefiniteNode: (path?: string) => boolean; + + /** Switch steps */ + switchSteps: (options: { draggedNodePath: string; droppedNodePath?: string }) => void; + /** Remove the step at a given path from the underlying Camel entity */ removeStep: (path?: string) => void; @@ -91,6 +97,12 @@ export interface IVisualizationNode implements Bas } } + isDefiniteNode(path?: string) { + if (path === 'route.from' || path === 'template.from') { + return false; + } else { + return true; + } + } + + switchSteps(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..4573b5c00 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,14 @@ export class CamelErrorHandlerVisualEntity implements BaseVisualCamelEntity { return; } + isDefiniteNode(_path?: string) { + return false; + } + + switchSteps(_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..893d3c5c4 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,14 @@ export class CamelRestConfigurationVisualEntity implements BaseVisualCamelEntity return; } + isDefiniteNode(_path?: string) { + return false; + } + + switchSteps(_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..9d6c9ab83 100644 --- a/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts @@ -167,6 +167,27 @@ export class PipeVisualEntity implements BaseVisualCamelEntity { } } + isDefiniteNode(path?: string) { + if (path === 'source' || path === 'sink') { + return false; + } else { + return true; + } + } + + switchSteps(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..584678f13 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