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 } : {}), }; });