mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(editor): Add support for maxConnections
in new canvas (no-changelog) (#10994)
This commit is contained in:
parent
b20d2eb403
commit
3ff3e05e75
|
@ -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),
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(() => {
|
|||
>
|
||||
<template v-for="source in mappedOutputs" :key="source.handleId">
|
||||
<CanvasHandleRenderer
|
||||
data-test-id="canvas-node-output-handle"
|
||||
v-bind="source"
|
||||
:mode="CanvasConnectionMode.Output"
|
||||
:type="source.type"
|
||||
:label="source.label"
|
||||
:index="source.index"
|
||||
:position="source.position"
|
||||
:offset="source.offset"
|
||||
:required="source.required"
|
||||
:is-connected="source.isConnected"
|
||||
:is-connecting="source.isConnecting"
|
||||
:is-read-only="readOnly"
|
||||
:is-valid-connection="isValidConnection"
|
||||
data-test-id="canvas-node-output-handle"
|
||||
@add="onAdd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-for="target in mappedInputs" :key="target.handleId">
|
||||
<CanvasHandleRenderer
|
||||
data-test-id="canvas-node-input-handle"
|
||||
v-bind="target"
|
||||
:mode="CanvasConnectionMode.Input"
|
||||
:type="target.type"
|
||||
:label="target.label"
|
||||
:index="target.index"
|
||||
:position="target.position"
|
||||
:offset="target.offset"
|
||||
:required="target.required"
|
||||
:is-connected="target.isConnected"
|
||||
:is-connecting="target.isConnecting"
|
||||
:is-read-only="readOnly"
|
||||
:is-valid-connection="isValidConnection"
|
||||
data-test-id="canvas-node-input-handle"
|
||||
@add="onAdd"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -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<NodeConnectionType>;
|
||||
index: Ref<number>;
|
||||
isRequired: Ref<boolean | undefined>;
|
||||
isConnected: Ref<boolean | undefined>;
|
||||
isConnected: ComputedRef<boolean | undefined>;
|
||||
isConnecting: Ref<boolean | undefined>;
|
||||
isReadOnly: Ref<boolean | undefined>;
|
||||
runData: Ref<ExecutionOutputMapData | undefined>;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 } : {}),
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue