diff --git a/packages/editor-ui/src/__tests__/data/canvas.ts b/packages/editor-ui/src/__tests__/data/canvas.ts
index 6c218e3617..2dfd644352 100644
--- a/packages/editor-ui/src/__tests__/data/canvas.ts
+++ b/packages/editor-ui/src/__tests__/data/canvas.ts
@@ -1,5 +1,5 @@
import { CanvasNodeHandleKey, CanvasNodeKey } from '@/constants';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import type {
CanvasNode,
CanvasNodeData,
@@ -143,7 +143,7 @@ export function createCanvasHandleProvide({
mode: ref(mode),
type: ref(type),
index: ref(index),
- isConnected: ref(isConnected),
+ isConnected: computed(() => isConnected),
isConnecting: ref(isConnecting),
isReadOnly: ref(isReadOnly),
isRequired: ref(isRequired),
diff --git a/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.vue b/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.vue
index 61c243e83d..a003e047b0 100644
--- a/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.vue
+++ b/packages/editor-ui/src/components/canvas/elements/handles/CanvasHandleRenderer.vue
@@ -14,19 +14,23 @@ import { CanvasNodeHandleKey } from '@/constants';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
import { useCanvasNode } from '@/composables/useCanvasNode';
-const props = defineProps<{
- mode: CanvasConnectionMode;
- isConnected?: boolean;
- isConnecting?: boolean;
- isReadOnly?: boolean;
- label?: string;
- required?: boolean;
- type: CanvasConnectionPort['type'];
- index: CanvasConnectionPort['index'];
- position: CanvasElementPortWithRenderData['position'];
- offset: CanvasElementPortWithRenderData['offset'];
- isValidConnection: ValidConnectionFunc;
-}>();
+const props = defineProps<
+ CanvasElementPortWithRenderData & {
+ type: CanvasConnectionPort['type'];
+ required?: CanvasConnectionPort['required'];
+ maxConnections?: CanvasConnectionPort['maxConnections'];
+ index: CanvasConnectionPort['index'];
+ label?: CanvasConnectionPort['label'];
+ handleId: CanvasElementPortWithRenderData['handleId'];
+ connectionsCount: CanvasElementPortWithRenderData['connectionsCount'];
+ isConnecting: CanvasElementPortWithRenderData['isConnecting'];
+ position: CanvasElementPortWithRenderData['position'];
+ offset?: CanvasElementPortWithRenderData['offset'];
+ mode: CanvasConnectionMode;
+ isReadOnly?: boolean;
+ isValidConnection: ValidConnectionFunc;
+ }
+>();
const emit = defineEmits<{
add: [handle: string];
@@ -50,15 +54,29 @@ const handleString = computed(() =>
}),
);
+const handleClasses = computed(() => [style.handle, style[props.type], style[props.mode]]);
+
+/**
+ * Connectable
+ */
+
+const connectionsLimitReached = computed(() => {
+ return props.maxConnections && props.connectionsCount >= props.maxConnections;
+});
+
const isConnectableStart = computed(() => {
+ if (connectionsLimitReached.value) return false;
+
return props.mode === CanvasConnectionMode.Output || props.type !== NodeConnectionType.Main;
});
const isConnectableEnd = computed(() => {
+ if (connectionsLimitReached.value) return false;
+
return props.mode === CanvasConnectionMode.Input || props.type !== NodeConnectionType.Main;
});
-const handleClasses = computed(() => [style.handle, style[props.type], style[props.mode]]);
+const isConnected = computed(() => props.connectionsCount > 0);
/**
* Run data
@@ -111,7 +129,6 @@ function onAdd() {
*/
const label = toRef(props, 'label');
-const isConnected = toRef(props, 'isConnected');
const isConnecting = toRef(props, 'isConnecting');
const isReadOnly = toRef(props, 'isReadOnly');
const mode = toRef(props, 'mode');
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 4a320f8b4e..2f03ed73ce 100644
--- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
+++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
@@ -154,7 +154,7 @@ const createEndpointMappingFn =
index: endpoint.index,
});
const handleType = mode === CanvasConnectionMode.Input ? 'target' : 'source';
- const isConnected = !!connections.value[mode][endpoint.type]?.[endpoint.index]?.length;
+ const connectionsCount = connections.value[mode][endpoint.type]?.[endpoint.index]?.length ?? 0;
const isConnecting =
connectingHandle.value?.nodeId === props.id &&
connectingHandle.value?.handleType === handleType &&
@@ -163,7 +163,7 @@ const createEndpointMappingFn =
return {
...endpoint,
handleId,
- isConnected,
+ connectionsCount,
isConnecting,
position,
offset: {
@@ -262,36 +262,22 @@ onBeforeUnmount(() => {
>
diff --git a/packages/editor-ui/src/types/canvas.ts b/packages/editor-ui/src/types/canvas.ts
index 7763cb8b50..bce5cc410e 100644
--- a/packages/editor-ui/src/types/canvas.ts
+++ b/packages/editor-ui/src/types/canvas.ts
@@ -7,7 +7,7 @@ import type {
} from 'n8n-workflow';
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
import type { IExecutionResponse, INodeUi } from '@/Interface';
-import type { Ref } from 'vue';
+import type { ComputedRef, Ref } from 'vue';
import type { PartialBy } from '@/utils/typeHelpers';
import type { EventBus } from 'n8n-design-system';
@@ -26,13 +26,14 @@ export const canvasConnectionModes = [
export type CanvasConnectionPort = {
type: CanvasConnectionPortType;
required?: boolean;
+ maxConnections?: number;
index: number;
label?: string;
};
export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
handleId: string;
- isConnected: boolean;
+ connectionsCount: number;
isConnecting: boolean;
position: Position;
offset?: { top?: string; left?: string };
@@ -166,7 +167,7 @@ export interface CanvasNodeHandleInjectionData {
type: Ref;
index: Ref;
isRequired: Ref;
- isConnected: Ref;
+ isConnected: ComputedRef;
isConnecting: Ref;
isReadOnly: Ref;
runData: Ref;
diff --git a/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts b/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts
index 9d84707513..f4a5b5c2b3 100644
--- a/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts
+++ b/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts
@@ -1,19 +1,19 @@
import {
- mapLegacyConnectionsToCanvasConnections,
- mapLegacyEndpointsToCanvasConnectionPort,
- getUniqueNodeName,
- mapCanvasConnectionToLegacyConnection,
- parseCanvasConnectionHandleString,
createCanvasConnectionHandleString,
createCanvasConnectionId,
+ getUniqueNodeName,
+ mapCanvasConnectionToLegacyConnection,
+ mapLegacyConnectionsToCanvasConnections,
+ mapLegacyEndpointsToCanvasConnectionPort,
+ parseCanvasConnectionHandleString,
checkOverlap,
} from '@/utils/canvasUtilsV2';
-import { NodeConnectionType, type IConnections, type INodeTypeDescription } from 'n8n-workflow';
+import { type IConnections, type INodeTypeDescription, NodeConnectionType } from 'n8n-workflow';
import type { CanvasConnection } from '@/types';
+import { CanvasConnectionMode } from '@/types';
import type { INodeUi } from '@/Interface';
import type { Connection } from '@vue-flow/core';
import { createTestNode } from '@/__tests__/mocks';
-import { CanvasConnectionMode } from '@/types';
vi.mock('uuid', () => ({
v4: vi.fn(() => 'mock-uuid'),
@@ -802,6 +802,29 @@ describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
{ type: NodeConnectionType.AiTool, index: 0, label: 'Optional Tool' },
]);
});
+
+ it('should map maxConnections correctly', () => {
+ const endpoints: INodeTypeDescription['inputs'] = [
+ NodeConnectionType.Main,
+ {
+ type: NodeConnectionType.AiMemory,
+ maxConnections: 1,
+ displayName: 'Optional Tool',
+ required: false,
+ },
+ ];
+ const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
+
+ expect(result).toEqual([
+ {
+ type: NodeConnectionType.Main,
+ maxConnections: undefined,
+ index: 0,
+ label: undefined,
+ },
+ { type: NodeConnectionType.AiMemory, maxConnections: 1, index: 0, label: 'Optional Tool' },
+ ]);
+ });
});
describe('getUniqueNodeName', () => {
diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtilsV2.ts
index e4c24f706a..4feeb6d2d6 100644
--- a/packages/editor-ui/src/utils/canvasUtilsV2.ts
+++ b/packages/editor-ui/src/utils/canvasUtilsV2.ts
@@ -183,11 +183,13 @@ export function mapLegacyEndpointsToCanvasConnectionPort(
.slice(0, endpointIndex + 1)
.filter((e) => (typeof e === 'string' ? e : e.type) === type).length - 1;
const required = typeof endpoint === 'string' ? false : endpoint.required;
+ const maxConnections = typeof endpoint === 'string' ? undefined : endpoint.maxConnections;
return {
type,
index,
label,
+ ...(maxConnections ? { maxConnections } : {}),
...(required ? { required } : {}),
};
});