diff --git a/packages/editor-ui/src/__tests__/data/canvas.ts b/packages/editor-ui/src/__tests__/data/canvas.ts index 10f36dc9ba..b61bf82c1d 100644 --- a/packages/editor-ui/src/__tests__/data/canvas.ts +++ b/packages/editor-ui/src/__tests__/data/canvas.ts @@ -154,6 +154,9 @@ export function createCanvasHandleProvide({ isReadOnly?: boolean; isRequired?: boolean; } = {}) { + const maxConnections = [NodeConnectionType.Main, NodeConnectionType.AiTool].includes(type) + ? Infinity + : 1; return { [String(CanvasNodeHandleKey)]: { label: ref(label), @@ -164,6 +167,7 @@ export function createCanvasHandleProvide({ isConnecting: ref(isConnecting), isReadOnly: ref(isReadOnly), isRequired: ref(isRequired), + maxConnections: ref(maxConnections), runData: ref(runData), } satisfies CanvasNodeHandleInjectionData, }; diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index f72f75b4e6..0d2b2cb4a3 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -29,10 +29,11 @@ import type { PinDataSource } from '@/composables/usePinnedData'; import { isPresent } from '@/utils/typesUtils'; import { GRID_SIZE } from '@/utils/nodeViewUtils'; import { CanvasKey } from '@/constants'; -import { onKeyDown, onKeyUp } from '@vueuse/core'; +import { onKeyDown, onKeyUp, useThrottleFn } from '@vueuse/core'; import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue'; import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue'; import { useCanvasTraversal } from '@/composables/useCanvasTraversal'; +import { NodeConnectionType } from 'n8n-workflow'; const $style = useCssModule(); @@ -115,6 +116,11 @@ const { onPaneReady, findNode, viewport, + onEdgeMouseLeave, + onEdgeMouseEnter, + onEdgeMouseMove, + onNodeMouseEnter, + onNodeMouseLeave, } = vueFlow; const { getIncomingNodes, @@ -297,7 +303,7 @@ function onUpdateNodeParameters(id: string, parameters: Record) } /** - * Connections + * Connections / Edges */ const connectionCreated = ref(false); @@ -339,6 +345,55 @@ function onClickConnectionAdd(connection: Connection) { const arrowHeadMarkerId = ref('custom-arrow-head'); +/** + * Edge and Nodes Hovering + */ + +const edgesHoveredById = ref>({}); +const edgesBringToFrontById = ref>({}); +const nodesHoveredById = ref>({}); + +onEdgeMouseEnter(({ edge }) => { + edgesBringToFrontById.value = { [edge.id]: true }; + edgesHoveredById.value = { [edge.id]: true }; +}); + +onEdgeMouseMove( + useThrottleFn(({ edge, event }) => { + const type = edge.data.source.type; + if (type !== NodeConnectionType.AiTool) { + return; + } + + if (!edge.data.maxConnections || edge.data.maxConnections > 1) { + const projectedPosition = getProjectedPosition(event); + const yDiff = projectedPosition.y - edge.targetY; + if (yDiff < 4 * GRID_SIZE) { + edgesBringToFrontById.value = { [edge.id]: false }; + } else { + edgesBringToFrontById.value = { [edge.id]: true }; + } + } + }, 100), +); + +onEdgeMouseLeave(({ edge }) => { + edgesBringToFrontById.value = { [edge.id]: false }; + edgesHoveredById.value = { [edge.id]: false }; +}); + +onNodeMouseEnter(({ node }) => { + nodesHoveredById.value = { [node.id]: true }; +}); + +onNodeMouseLeave(({ node }) => { + nodesHoveredById.value = { [node.id]: false }; +}); + +function onUpdateEdgeHovered(id: string, hovered: boolean) { + edgesHoveredById.value[id] = hovered; +} + /** * Executions */ @@ -375,7 +430,7 @@ const defaultZoom = 1; const zoom = ref(defaultZoom); const isPaneMoving = ref(false); -function getProjectedPosition(event?: MouseEvent) { +function getProjectedPosition(event?: Pick) { const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }; const offsetX = event?.clientX ?? 0; const offsetY = event?.clientY ?? 0; @@ -590,11 +645,13 @@ provide(CanvasKey, { @move-end="onPaneMoveEnd" @node-drag-stop="onNodeDragStop" > -