diff --git a/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.vue b/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.vue index 767c52a4a..f1c42532e 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.vue +++ b/packages/editor-ui/src/components/canvas/elements/handles/HandleRenderer.vue @@ -2,6 +2,7 @@ import { computed, h, provide, toRef, useCssModule } from 'vue'; import type { CanvasConnectionPort, CanvasElementPortWithPosition } from '@/types'; +import type { ValidConnectionFunc } from '@vue-flow/core'; import { Handle } from '@vue-flow/core'; import { NodeConnectionType } from 'n8n-workflow'; import CanvasHandleMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue'; @@ -16,6 +17,7 @@ const props = defineProps<{ index: CanvasConnectionPort['index']; position: CanvasElementPortWithPosition['position']; offset: CanvasElementPortWithPosition['offset']; + isValidConnection: ValidConnectionFunc; }>(); const $style = useCssModule(); @@ -66,6 +68,7 @@ provide(CanvasNodeHandleKey, { :style="offset" :connectable-start="isConnectableStart" :connectable-end="isConnectableEnd" + :is-valid-connection="isValidConnection" > diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue index 5f419e511..e3cf9829b 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue @@ -27,11 +27,12 @@ const nodeTypesStore = useNodeTypesStore(); const inputs = computed(() => props.data.inputs); const outputs = computed(() => props.data.outputs); const connections = computed(() => props.data.connections); -const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs } = useNodeConnections({ - inputs, - outputs, - connections, -}); +const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs, isValidConnection } = + useNodeConnections({ + inputs, + outputs, + connections, + }); const isDisabled = computed(() => props.data.disabled); @@ -39,13 +40,6 @@ const nodeType = computed(() => { return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion); }); -watch( - () => props.selected, - (selected) => { - emit('select', props.id, selected); - }, -); - /** * Inputs */ @@ -68,6 +62,14 @@ const outputsWithPosition = computed(() => { ]; }); +/** + * Node icon + */ + +const nodeIconSize = computed(() => + 'configuration' in data.value.render.options && data.value.render.options.configuration ? 30 : 40, +); + /** * Endpoints */ @@ -89,26 +91,9 @@ const mapEndpointWithPosition = }; /** - * Provide + * Events */ -const id = toRef(props, 'id'); -const data = toRef(props, 'data'); -const label = toRef(props, 'label'); -const selected = toRef(props, 'selected'); - -provide(CanvasNodeKey, { - id, - data, - label, - selected, - nodeType, -}); - -const nodeIconSize = computed(() => - 'configuration' in data.value.render.options && data.value.render.options.configuration ? 30 : 40, -); - function onDelete() { emit('delete', props.id); } @@ -132,6 +117,34 @@ function onUpdate(parameters: Record) { function onMove(position: XYPosition) { emit('move', props.id, position); } + +/** + * Provide + */ + +const id = toRef(props, 'id'); +const data = toRef(props, 'data'); +const label = toRef(props, 'label'); +const selected = toRef(props, 'selected'); + +provide(CanvasNodeKey, { + id, + data, + label, + selected, + nodeType, +}); + +/** + * Lifecycle + */ + +watch( + () => props.selected, + (selected) => { + emit('select', props.id, selected); + }, +); @@ -157,6 +171,7 @@ function onMove(position: XYPosition) { :index="target.index" :position="target.position" :offset="target.offset" + :is-valid-connection="isValidConnection" /> diff --git a/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts b/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts index d85f3a18c..05fe2738f 100644 --- a/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useNodeConnections.spec.ts @@ -2,6 +2,8 @@ import { ref } from 'vue'; import { NodeConnectionType } from 'n8n-workflow'; import { useNodeConnections } from '@/composables/useNodeConnections'; import type { CanvasNodeData } from '@/types'; +import { CanvasConnectionMode } from '@/types'; +import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; describe('useNodeConnections', () => { const defaultConnections = { input: {}, output: {} }; @@ -158,4 +160,87 @@ describe('useNodeConnections', () => { ); }); }); + + describe('isValidConnection', () => { + const inputs = ref([]); + const outputs = ref([]); + + const { isValidConnection } = useNodeConnections({ + inputs, + outputs, + connections: defaultConnections, + }); + + it('returns false if source and target nodes are the same', () => { + const connection = { + source: 'node1', + target: 'node1', + sourceHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Output, + type: NodeConnectionType.Main, + index: 0, + }), + targetHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Input, + type: NodeConnectionType.Main, + index: 0, + }), + }; + expect(isValidConnection(connection)).toBe(false); + }); + + it('returns false if source and target handles are of the same mode', () => { + const connection = { + source: 'node1', + target: 'node2', + sourceHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Output, + type: NodeConnectionType.Main, + index: 0, + }), + targetHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Output, + type: NodeConnectionType.Main, + index: 0, + }), + }; + expect(isValidConnection(connection)).toBe(false); + }); + + it('returns false if source and target handles are of different types', () => { + const connection = { + source: 'node1', + target: 'node2', + sourceHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Output, + type: NodeConnectionType.Main, + index: 0, + }), + targetHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Input, + type: NodeConnectionType.AiMemory, + index: 0, + }), + }; + expect(isValidConnection(connection)).toBe(false); + }); + + it('returns true if source and target nodes are different, modes are different, and types are the same', () => { + const connection = { + source: 'node1', + target: 'node2', + sourceHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Output, + type: NodeConnectionType.Main, + index: 0, + }), + targetHandle: createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Input, + type: NodeConnectionType.Main, + index: 0, + }), + }; + expect(isValidConnection(connection)).toBe(true); + }); + }); }); diff --git a/packages/editor-ui/src/composables/useNodeConnections.ts b/packages/editor-ui/src/composables/useNodeConnections.ts index ac7cd858b..57c94ca87 100644 --- a/packages/editor-ui/src/composables/useNodeConnections.ts +++ b/packages/editor-ui/src/composables/useNodeConnections.ts @@ -2,6 +2,8 @@ import type { CanvasNodeData } from '@/types'; import type { MaybeRef } from 'vue'; import { computed, unref } from 'vue'; import { NodeConnectionType } from 'n8n-workflow'; +import type { Connection } from '@vue-flow/core'; +import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; export function useNodeConnections({ inputs, @@ -48,6 +50,25 @@ export function useNodeConnections({ () => unref(connections).output[NodeConnectionType.Main] ?? [], ); + /** + * Connection validation + */ + + function isValidConnection(connection: Connection) { + const { type: sourceType, mode: sourceMode } = parseCanvasConnectionHandleString( + connection.sourceHandle, + ); + const { type: targetType, mode: targetMode } = parseCanvasConnectionHandleString( + connection.targetHandle, + ); + + const isSameNode = connection.source === connection.target; + const isSameMode = sourceMode === targetMode; + const isSameType = sourceType === targetType; + + return !isSameNode && !isSameMode && isSameType; + } + return { mainInputs, nonMainInputs, @@ -56,5 +77,6 @@ export function useNodeConnections({ mainOutputs, nonMainOutputs, mainOutputConnections, + isValidConnection, }; }