feat(editor): Add support for maxConnections in new canvas (no-changelog) (#10994)

This commit is contained in:
Alex Grozav 2024-09-30 15:23:51 +03:00 committed by GitHub
parent b20d2eb403
commit 3ff3e05e75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 76 additions and 47 deletions

View file

@ -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),

View file

@ -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');

View file

@ -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>

View file

@ -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>;

View file

@ -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', () => {

View file

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