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 { CanvasNodeHandleKey, CanvasNodeKey } from '@/constants';
import { ref } from 'vue'; import { computed, ref } from 'vue';
import type { import type {
CanvasNode, CanvasNode,
CanvasNodeData, CanvasNodeData,
@ -143,7 +143,7 @@ export function createCanvasHandleProvide({
mode: ref(mode), mode: ref(mode),
type: ref(type), type: ref(type),
index: ref(index), index: ref(index),
isConnected: ref(isConnected), isConnected: computed(() => isConnected),
isConnecting: ref(isConnecting), isConnecting: ref(isConnecting),
isReadOnly: ref(isReadOnly), isReadOnly: ref(isReadOnly),
isRequired: ref(isRequired), isRequired: ref(isRequired),

View file

@ -14,19 +14,23 @@ import { CanvasNodeHandleKey } from '@/constants';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
import { useCanvasNode } from '@/composables/useCanvasNode'; import { useCanvasNode } from '@/composables/useCanvasNode';
const props = defineProps<{ const props = defineProps<
mode: CanvasConnectionMode; CanvasElementPortWithRenderData & {
isConnected?: boolean; type: CanvasConnectionPort['type'];
isConnecting?: boolean; required?: CanvasConnectionPort['required'];
isReadOnly?: boolean; maxConnections?: CanvasConnectionPort['maxConnections'];
label?: string; index: CanvasConnectionPort['index'];
required?: boolean; label?: CanvasConnectionPort['label'];
type: CanvasConnectionPort['type']; handleId: CanvasElementPortWithRenderData['handleId'];
index: CanvasConnectionPort['index']; connectionsCount: CanvasElementPortWithRenderData['connectionsCount'];
position: CanvasElementPortWithRenderData['position']; isConnecting: CanvasElementPortWithRenderData['isConnecting'];
offset: CanvasElementPortWithRenderData['offset']; position: CanvasElementPortWithRenderData['position'];
isValidConnection: ValidConnectionFunc; offset?: CanvasElementPortWithRenderData['offset'];
}>(); mode: CanvasConnectionMode;
isReadOnly?: boolean;
isValidConnection: ValidConnectionFunc;
}
>();
const emit = defineEmits<{ const emit = defineEmits<{
add: [handle: string]; 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(() => { const isConnectableStart = computed(() => {
if (connectionsLimitReached.value) return false;
return props.mode === CanvasConnectionMode.Output || props.type !== NodeConnectionType.Main; return props.mode === CanvasConnectionMode.Output || props.type !== NodeConnectionType.Main;
}); });
const isConnectableEnd = computed(() => { const isConnectableEnd = computed(() => {
if (connectionsLimitReached.value) return false;
return props.mode === CanvasConnectionMode.Input || props.type !== NodeConnectionType.Main; 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 * Run data
@ -111,7 +129,6 @@ function onAdd() {
*/ */
const label = toRef(props, 'label'); const label = toRef(props, 'label');
const isConnected = toRef(props, 'isConnected');
const isConnecting = toRef(props, 'isConnecting'); const isConnecting = toRef(props, 'isConnecting');
const isReadOnly = toRef(props, 'isReadOnly'); const isReadOnly = toRef(props, 'isReadOnly');
const mode = toRef(props, 'mode'); const mode = toRef(props, 'mode');

View file

@ -154,7 +154,7 @@ const createEndpointMappingFn =
index: endpoint.index, index: endpoint.index,
}); });
const handleType = mode === CanvasConnectionMode.Input ? 'target' : 'source'; 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 = const isConnecting =
connectingHandle.value?.nodeId === props.id && connectingHandle.value?.nodeId === props.id &&
connectingHandle.value?.handleType === handleType && connectingHandle.value?.handleType === handleType &&
@ -163,7 +163,7 @@ const createEndpointMappingFn =
return { return {
...endpoint, ...endpoint,
handleId, handleId,
isConnected, connectionsCount,
isConnecting, isConnecting,
position, position,
offset: { offset: {
@ -262,36 +262,22 @@ onBeforeUnmount(() => {
> >
<template v-for="source in mappedOutputs" :key="source.handleId"> <template v-for="source in mappedOutputs" :key="source.handleId">
<CanvasHandleRenderer <CanvasHandleRenderer
data-test-id="canvas-node-output-handle" v-bind="source"
:mode="CanvasConnectionMode.Output" :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-read-only="readOnly"
:is-valid-connection="isValidConnection" :is-valid-connection="isValidConnection"
data-test-id="canvas-node-output-handle"
@add="onAdd" @add="onAdd"
/> />
</template> </template>
<template v-for="target in mappedInputs" :key="target.handleId"> <template v-for="target in mappedInputs" :key="target.handleId">
<CanvasHandleRenderer <CanvasHandleRenderer
data-test-id="canvas-node-input-handle" v-bind="target"
:mode="CanvasConnectionMode.Input" :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-read-only="readOnly"
:is-valid-connection="isValidConnection" :is-valid-connection="isValidConnection"
data-test-id="canvas-node-input-handle"
@add="onAdd" @add="onAdd"
/> />
</template> </template>

View file

@ -7,7 +7,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core'; import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
import type { IExecutionResponse, INodeUi } from '@/Interface'; 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 { PartialBy } from '@/utils/typeHelpers';
import type { EventBus } from 'n8n-design-system'; import type { EventBus } from 'n8n-design-system';
@ -26,13 +26,14 @@ export const canvasConnectionModes = [
export type CanvasConnectionPort = { export type CanvasConnectionPort = {
type: CanvasConnectionPortType; type: CanvasConnectionPortType;
required?: boolean; required?: boolean;
maxConnections?: number;
index: number; index: number;
label?: string; label?: string;
}; };
export interface CanvasElementPortWithRenderData extends CanvasConnectionPort { export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
handleId: string; handleId: string;
isConnected: boolean; connectionsCount: number;
isConnecting: boolean; isConnecting: boolean;
position: Position; position: Position;
offset?: { top?: string; left?: string }; offset?: { top?: string; left?: string };
@ -166,7 +167,7 @@ export interface CanvasNodeHandleInjectionData {
type: Ref<NodeConnectionType>; type: Ref<NodeConnectionType>;
index: Ref<number>; index: Ref<number>;
isRequired: Ref<boolean | undefined>; isRequired: Ref<boolean | undefined>;
isConnected: Ref<boolean | undefined>; isConnected: ComputedRef<boolean | undefined>;
isConnecting: Ref<boolean | undefined>; isConnecting: Ref<boolean | undefined>;
isReadOnly: Ref<boolean | undefined>; isReadOnly: Ref<boolean | undefined>;
runData: Ref<ExecutionOutputMapData | undefined>; runData: Ref<ExecutionOutputMapData | undefined>;

View file

@ -1,19 +1,19 @@
import { import {
mapLegacyConnectionsToCanvasConnections,
mapLegacyEndpointsToCanvasConnectionPort,
getUniqueNodeName,
mapCanvasConnectionToLegacyConnection,
parseCanvasConnectionHandleString,
createCanvasConnectionHandleString, createCanvasConnectionHandleString,
createCanvasConnectionId, createCanvasConnectionId,
getUniqueNodeName,
mapCanvasConnectionToLegacyConnection,
mapLegacyConnectionsToCanvasConnections,
mapLegacyEndpointsToCanvasConnectionPort,
parseCanvasConnectionHandleString,
checkOverlap, checkOverlap,
} from '@/utils/canvasUtilsV2'; } 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 type { CanvasConnection } from '@/types';
import { CanvasConnectionMode } from '@/types';
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import type { Connection } from '@vue-flow/core'; import type { Connection } from '@vue-flow/core';
import { createTestNode } from '@/__tests__/mocks'; import { createTestNode } from '@/__tests__/mocks';
import { CanvasConnectionMode } from '@/types';
vi.mock('uuid', () => ({ vi.mock('uuid', () => ({
v4: vi.fn(() => 'mock-uuid'), v4: vi.fn(() => 'mock-uuid'),
@ -802,6 +802,29 @@ describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
{ type: NodeConnectionType.AiTool, index: 0, label: 'Optional Tool' }, { 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', () => { describe('getUniqueNodeName', () => {

View file

@ -183,11 +183,13 @@ export function mapLegacyEndpointsToCanvasConnectionPort(
.slice(0, endpointIndex + 1) .slice(0, endpointIndex + 1)
.filter((e) => (typeof e === 'string' ? e : e.type) === type).length - 1; .filter((e) => (typeof e === 'string' ? e : e.type) === type).length - 1;
const required = typeof endpoint === 'string' ? false : endpoint.required; const required = typeof endpoint === 'string' ? false : endpoint.required;
const maxConnections = typeof endpoint === 'string' ? undefined : endpoint.maxConnections;
return { return {
type, type,
index, index,
label, label,
...(maxConnections ? { maxConnections } : {}),
...(required ? { required } : {}), ...(required ? { required } : {}),
}; };
}); });