feat(editor): Add support for connection validation (no-changelog) (#10059)

This commit is contained in:
Alex Grozav 2024-07-15 18:32:02 +03:00 committed by GitHub
parent 0d12f0a6b3
commit bb354f3c88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 155 additions and 30 deletions

View file

@ -2,6 +2,7 @@
import { computed, h, provide, toRef, useCssModule } from 'vue';
import type { CanvasConnectionPort, CanvasElementPortWithPosition } from '@/types';
import type { ValidConnectionFunc } from '@vue-flow/core';
import { Handle } from '@vue-flow/core';
import { NodeConnectionType } from 'n8n-workflow';
import CanvasHandleMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue';
@ -16,6 +17,7 @@ const props = defineProps<{
index: CanvasConnectionPort['index'];
position: CanvasElementPortWithPosition['position'];
offset: CanvasElementPortWithPosition['offset'];
isValidConnection: ValidConnectionFunc;
}>();
const $style = useCssModule();
@ -66,6 +68,7 @@ provide(CanvasNodeHandleKey, {
:style="offset"
:connectable-start="isConnectableStart"
:connectable-end="isConnectableEnd"
:is-valid-connection="isValidConnection"
>
<Render :label="label" />
</Handle>

View file

@ -27,11 +27,12 @@ const nodeTypesStore = useNodeTypesStore();
const inputs = computed(() => props.data.inputs);
const outputs = computed(() => props.data.outputs);
const connections = computed(() => props.data.connections);
const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs } = useNodeConnections({
inputs,
outputs,
connections,
});
const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs, isValidConnection } =
useNodeConnections({
inputs,
outputs,
connections,
});
const isDisabled = computed(() => props.data.disabled);
@ -39,13 +40,6 @@ const nodeType = computed(() => {
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
});
watch(
() => props.selected,
(selected) => {
emit('select', props.id, selected);
},
);
/**
* Inputs
*/
@ -68,6 +62,14 @@ const outputsWithPosition = computed(() => {
];
});
/**
* Node icon
*/
const nodeIconSize = computed(() =>
'configuration' in data.value.render.options && data.value.render.options.configuration ? 30 : 40,
);
/**
* Endpoints
*/
@ -89,26 +91,9 @@ const mapEndpointWithPosition =
};
/**
* Provide
* Events
*/
const id = toRef(props, 'id');
const data = toRef(props, 'data');
const label = toRef(props, 'label');
const selected = toRef(props, 'selected');
provide(CanvasNodeKey, {
id,
data,
label,
selected,
nodeType,
});
const nodeIconSize = computed(() =>
'configuration' in data.value.render.options && data.value.render.options.configuration ? 30 : 40,
);
function onDelete() {
emit('delete', props.id);
}
@ -132,6 +117,34 @@ function onUpdate(parameters: Record<string, unknown>) {
function onMove(position: XYPosition) {
emit('move', props.id, position);
}
/**
* Provide
*/
const id = toRef(props, 'id');
const data = toRef(props, 'data');
const label = toRef(props, 'label');
const selected = toRef(props, 'selected');
provide(CanvasNodeKey, {
id,
data,
label,
selected,
nodeType,
});
/**
* Lifecycle
*/
watch(
() => props.selected,
(selected) => {
emit('select', props.id, selected);
},
);
</script>
<template>
@ -145,6 +158,7 @@ function onMove(position: XYPosition) {
:index="source.index"
:position="source.position"
:offset="source.offset"
:is-valid-connection="isValidConnection"
/>
</template>
@ -157,6 +171,7 @@ function onMove(position: XYPosition) {
:index="target.index"
:position="target.position"
:offset="target.offset"
:is-valid-connection="isValidConnection"
/>
</template>

View file

@ -2,6 +2,8 @@ import { ref } from 'vue';
import { NodeConnectionType } from 'n8n-workflow';
import { useNodeConnections } from '@/composables/useNodeConnections';
import type { CanvasNodeData } from '@/types';
import { CanvasConnectionMode } from '@/types';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
describe('useNodeConnections', () => {
const defaultConnections = { input: {}, output: {} };
@ -158,4 +160,87 @@ describe('useNodeConnections', () => {
);
});
});
describe('isValidConnection', () => {
const inputs = ref<CanvasNodeData['inputs']>([]);
const outputs = ref<CanvasNodeData['outputs']>([]);
const { isValidConnection } = useNodeConnections({
inputs,
outputs,
connections: defaultConnections,
});
it('returns false if source and target nodes are the same', () => {
const connection = {
source: 'node1',
target: 'node1',
sourceHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Output,
type: NodeConnectionType.Main,
index: 0,
}),
targetHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Input,
type: NodeConnectionType.Main,
index: 0,
}),
};
expect(isValidConnection(connection)).toBe(false);
});
it('returns false if source and target handles are of the same mode', () => {
const connection = {
source: 'node1',
target: 'node2',
sourceHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Output,
type: NodeConnectionType.Main,
index: 0,
}),
targetHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Output,
type: NodeConnectionType.Main,
index: 0,
}),
};
expect(isValidConnection(connection)).toBe(false);
});
it('returns false if source and target handles are of different types', () => {
const connection = {
source: 'node1',
target: 'node2',
sourceHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Output,
type: NodeConnectionType.Main,
index: 0,
}),
targetHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Input,
type: NodeConnectionType.AiMemory,
index: 0,
}),
};
expect(isValidConnection(connection)).toBe(false);
});
it('returns true if source and target nodes are different, modes are different, and types are the same', () => {
const connection = {
source: 'node1',
target: 'node2',
sourceHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Output,
type: NodeConnectionType.Main,
index: 0,
}),
targetHandle: createCanvasConnectionHandleString({
mode: CanvasConnectionMode.Input,
type: NodeConnectionType.Main,
index: 0,
}),
};
expect(isValidConnection(connection)).toBe(true);
});
});
});

View file

@ -2,6 +2,8 @@ import type { CanvasNodeData } from '@/types';
import type { MaybeRef } from 'vue';
import { computed, unref } from 'vue';
import { NodeConnectionType } from 'n8n-workflow';
import type { Connection } from '@vue-flow/core';
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
export function useNodeConnections({
inputs,
@ -48,6 +50,25 @@ export function useNodeConnections({
() => unref(connections).output[NodeConnectionType.Main] ?? [],
);
/**
* Connection validation
*/
function isValidConnection(connection: Connection) {
const { type: sourceType, mode: sourceMode } = parseCanvasConnectionHandleString(
connection.sourceHandle,
);
const { type: targetType, mode: targetMode } = parseCanvasConnectionHandleString(
connection.targetHandle,
);
const isSameNode = connection.source === connection.target;
const isSameMode = sourceMode === targetMode;
const isSameType = sourceType === targetType;
return !isSameNode && !isSameMode && isSameType;
}
return {
mainInputs,
nonMainInputs,
@ -56,5 +77,6 @@ export function useNodeConnections({
mainOutputs,
nonMainOutputs,
mainOutputConnections,
isValidConnection,
};
}