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);
+ },
+);
@@ -145,6 +158,7 @@ function onMove(position: XYPosition) {
:index="source.index"
:position="source.position"
:offset="source.offset"
+ :is-valid-connection="isValidConnection"
/>
@@ -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,
};
}