diff --git a/web/app/components/workflow/hooks/use-helpline.ts b/web/app/components/workflow/hooks/use-helpline.ts index 2eed71a807..96a85aabd0 100644 --- a/web/app/components/workflow/hooks/use-helpline.ts +++ b/web/app/components/workflow/hooks/use-helpline.ts @@ -1,12 +1,40 @@ import { useCallback } from 'react' import { useStoreApi } from 'reactflow' import type { Node } from '../types' +import { BlockEnum, TRIGGER_NODE_TYPES } from '../types' import { useWorkflowStore } from '../store' +// Entry node (Start/Trigger) wrapper offsets +// The EntryNodeContainer adds a wrapper with status indicator above the actual node +// These offsets ensure alignment happens on the inner node, not the wrapper +const ENTRY_NODE_WRAPPER_OFFSET = { + x: 0, // No horizontal padding on wrapper (px-0) + y: 21, // Actual measured: pt-0.5 (2px) + status bar height (~19px) +} as const + export const useHelpline = () => { const store = useStoreApi() const workflowStore = useWorkflowStore() + // Check if a node is an entry node (Start or Trigger) + const isEntryNode = useCallback((node: Node): boolean => { + return TRIGGER_NODE_TYPES.includes(node.data.type as any) || node.data.type === BlockEnum.Start + }, []) + + // Get the actual alignment position of a node (accounting for wrapper offset) + const getNodeAlignPosition = useCallback((node: Node) => { + if (isEntryNode(node)) { + return { + x: node.position.x + ENTRY_NODE_WRAPPER_OFFSET.x, + y: node.position.y + ENTRY_NODE_WRAPPER_OFFSET.y, + } + } + return { + x: node.position.x, + y: node.position.y, + } + }, [isEntryNode]) + const handleSetHelpline = useCallback((node: Node) => { const { getNodes } = store.getState() const nodes = getNodes() @@ -29,6 +57,9 @@ export const useHelpline = () => { } } + // Get the actual alignment position for the dragging node + const nodeAlignPos = getNodeAlignPosition(node) + const showHorizontalHelpLineNodes = nodes.filter((n) => { if (n.id === node.id) return false @@ -39,33 +70,52 @@ export const useHelpline = () => { if (n.data.isInLoop) return false - const nY = Math.ceil(n.position.y) - const nodeY = Math.ceil(node.position.y) + // Get actual alignment position for comparison node + const nAlignPos = getNodeAlignPosition(n) + const nY = Math.ceil(nAlignPos.y) + const nodeY = Math.ceil(nodeAlignPos.y) if (nY - nodeY < 5 && nY - nodeY > -5) return true return false - }).sort((a, b) => a.position.x - b.position.x) + }).sort((a, b) => { + const aPos = getNodeAlignPosition(a) + const bPos = getNodeAlignPosition(b) + return aPos.x - bPos.x + }) const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length if (showHorizontalHelpLineNodesLength > 0) { const first = showHorizontalHelpLineNodes[0] const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1] + // Use actual alignment positions for help line rendering + const firstPos = getNodeAlignPosition(first) + const lastPos = getNodeAlignPosition(last) + + // For entry nodes, we need to subtract the offset from width since lastPos already includes it + const lastIsEntryNode = isEntryNode(last) + const lastNodeWidth = lastIsEntryNode ? last.width! - ENTRY_NODE_WRAPPER_OFFSET.x : last.width! + const helpLine = { - top: first.position.y, - left: first.position.x, - width: last.position.x + last.width! - first.position.x, + top: firstPos.y, + left: firstPos.x, + width: lastPos.x + lastNodeWidth - firstPos.x, } - if (node.position.x < first.position.x) { - helpLine.left = node.position.x - helpLine.width = first.position.x + first.width! - node.position.x + if (nodeAlignPos.x < firstPos.x) { + const firstIsEntryNode = isEntryNode(first) + const firstNodeWidth = firstIsEntryNode ? first.width! - ENTRY_NODE_WRAPPER_OFFSET.x : first.width! + helpLine.left = nodeAlignPos.x + helpLine.width = firstPos.x + firstNodeWidth - nodeAlignPos.x } - if (node.position.x > last.position.x) - helpLine.width = node.position.x + node.width! - first.position.x + if (nodeAlignPos.x > lastPos.x) { + const nodeIsEntryNode = isEntryNode(node) + const nodeWidth = nodeIsEntryNode ? node.width! - ENTRY_NODE_WRAPPER_OFFSET.x : node.width! + helpLine.width = nodeAlignPos.x + nodeWidth - firstPos.x + } setHelpLineHorizontal(helpLine) } @@ -81,33 +131,52 @@ export const useHelpline = () => { if (n.data.isInLoop) return false - const nX = Math.ceil(n.position.x) - const nodeX = Math.ceil(node.position.x) + // Get actual alignment position for comparison node + const nAlignPos = getNodeAlignPosition(n) + const nX = Math.ceil(nAlignPos.x) + const nodeX = Math.ceil(nodeAlignPos.x) if (nX - nodeX < 5 && nX - nodeX > -5) return true return false - }).sort((a, b) => a.position.x - b.position.x) + }).sort((a, b) => { + const aPos = getNodeAlignPosition(a) + const bPos = getNodeAlignPosition(b) + return aPos.x - bPos.x + }) const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length if (showVerticalHelpLineNodesLength > 0) { const first = showVerticalHelpLineNodes[0] const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1] + // Use actual alignment positions for help line rendering + const firstPos = getNodeAlignPosition(first) + const lastPos = getNodeAlignPosition(last) + + // For entry nodes, we need to subtract the offset from height since lastPos already includes it + const lastIsEntryNode = isEntryNode(last) + const lastNodeHeight = lastIsEntryNode ? last.height! - ENTRY_NODE_WRAPPER_OFFSET.y : last.height! + const helpLine = { - top: first.position.y, - left: first.position.x, - height: last.position.y + last.height! - first.position.y, + top: firstPos.y, + left: firstPos.x, + height: lastPos.y + lastNodeHeight - firstPos.y, } - if (node.position.y < first.position.y) { - helpLine.top = node.position.y - helpLine.height = first.position.y + first.height! - node.position.y + if (nodeAlignPos.y < firstPos.y) { + const firstIsEntryNode = isEntryNode(first) + const firstNodeHeight = firstIsEntryNode ? first.height! - ENTRY_NODE_WRAPPER_OFFSET.y : first.height! + helpLine.top = nodeAlignPos.y + helpLine.height = firstPos.y + firstNodeHeight - nodeAlignPos.y } - if (node.position.y > last.position.y) - helpLine.height = node.position.y + node.height! - first.position.y + if (nodeAlignPos.y > lastPos.y) { + const nodeIsEntryNode = isEntryNode(node) + const nodeHeight = nodeIsEntryNode ? node.height! - ENTRY_NODE_WRAPPER_OFFSET.y : node.height! + helpLine.height = nodeAlignPos.y + nodeHeight - firstPos.y + } setHelpLineVertical(helpLine) } @@ -119,7 +188,7 @@ export const useHelpline = () => { showHorizontalHelpLineNodes, showVerticalHelpLineNodes, } - }, [store, workflowStore]) + }, [store, workflowStore, getNodeAlignPosition]) return { handleSetHelpline, diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 162e6d0b5c..299d5e339d 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -18,7 +18,7 @@ import { } from 'reactflow' import type { PluginDefaultValue } from '../block-selector/types' import type { Edge, Node, OnNodeAdd } from '../types' -import { BlockEnum } from '../types' +import { BlockEnum, TRIGGER_NODE_TYPES } from '../types' import { useWorkflowStore } from '../store' import { CUSTOM_EDGE, @@ -65,6 +65,13 @@ import { getNodeUsedVars } from '../nodes/_base/components/variable/utils' // Entry node deletion restriction has been removed to allow empty workflows +// Entry node (Start/Trigger) wrapper offsets for alignment +// Must match the values in use-helpline.ts +const ENTRY_NODE_WRAPPER_OFFSET = { + x: 0, + y: 21, // Adjusted based on visual testing feedback +} as const + export const useNodesInteractions = () => { const { t } = useTranslation() const store = useStoreApi() @@ -140,21 +147,51 @@ export const useNodesInteractions = () => { const newNodes = produce(nodes, (draft) => { const currentNode = draft.find(n => n.id === node.id)! - if (showVerticalHelpLineNodesLength > 0) - currentNode.position.x = showVerticalHelpLineNodes[0].position.x - else if (restrictPosition.x !== undefined) - currentNode.position.x = restrictPosition.x - else if (restrictLoopPosition.x !== undefined) - currentNode.position.x = restrictLoopPosition.x - else currentNode.position.x = node.position.x + // Check if current dragging node is an entry node + const isCurrentEntryNode = TRIGGER_NODE_TYPES.includes(node.data.type as any) || node.data.type === BlockEnum.Start - if (showHorizontalHelpLineNodesLength > 0) - currentNode.position.y = showHorizontalHelpLineNodes[0].position.y - else if (restrictPosition.y !== undefined) + // X-axis alignment with offset consideration + if (showVerticalHelpLineNodesLength > 0) { + const targetNode = showVerticalHelpLineNodes[0] + const isTargetEntryNode = TRIGGER_NODE_TYPES.includes(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start + + // Calculate the wrapper position needed to align the inner nodes + // Target inner position = target.position + target.offset + // Current inner position should equal target inner position + // So: current.position + current.offset = target.position + target.offset + // Therefore: current.position = target.position + target.offset - current.offset + const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0 + const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0 + currentNode.position.x = targetNode.position.x + targetOffset - currentOffset + } + else if (restrictPosition.x !== undefined) { + currentNode.position.x = restrictPosition.x + } + else if (restrictLoopPosition.x !== undefined) { + currentNode.position.x = restrictLoopPosition.x + } + else { + currentNode.position.x = node.position.x + } + + // Y-axis alignment with offset consideration + if (showHorizontalHelpLineNodesLength > 0) { + const targetNode = showHorizontalHelpLineNodes[0] + const isTargetEntryNode = TRIGGER_NODE_TYPES.includes(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start + + const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0 + const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0 + currentNode.position.y = targetNode.position.y + targetOffset - currentOffset + } + else if (restrictPosition.y !== undefined) { currentNode.position.y = restrictPosition.y - else if (restrictLoopPosition.y !== undefined) + } + else if (restrictLoopPosition.y !== undefined) { currentNode.position.y = restrictLoopPosition.y - else currentNode.position.y = node.position.y + } + else { + currentNode.position.y = node.position.y + } }) setNodes(newNodes) },