mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-03 17:07:29 -08:00
fix(editor): Remove invalid connections after node handles change (#12247)
This commit is contained in:
parent
4d8e9cfc61
commit
6330bec4db
|
@ -39,6 +39,8 @@ const emit = defineEmits<{
|
||||||
'update:node:selected': [id: string];
|
'update:node:selected': [id: string];
|
||||||
'update:node:name': [id: string];
|
'update:node:name': [id: string];
|
||||||
'update:node:parameters': [id: string, parameters: Record<string, unknown>];
|
'update:node:parameters': [id: string, parameters: Record<string, unknown>];
|
||||||
|
'update:node:inputs': [id: string];
|
||||||
|
'update:node:outputs': [id: string];
|
||||||
'click:node:add': [id: string, handle: string];
|
'click:node:add': [id: string, handle: string];
|
||||||
'run:node': [id: string];
|
'run:node': [id: string];
|
||||||
'delete:node': [id: string];
|
'delete:node': [id: string];
|
||||||
|
@ -302,6 +304,14 @@ function onUpdateNodeParameters(id: string, parameters: Record<string, unknown>)
|
||||||
emit('update:node:parameters', id, parameters);
|
emit('update:node:parameters', id, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUpdateNodeInputs(id: string) {
|
||||||
|
emit('update:node:inputs', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdateNodeOutputs(id: string) {
|
||||||
|
emit('update:node:outputs', id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connections / Edges
|
* Connections / Edges
|
||||||
*/
|
*/
|
||||||
|
@ -679,6 +689,8 @@ provide(CanvasKey, {
|
||||||
@activate="onSetNodeActive"
|
@activate="onSetNodeActive"
|
||||||
@open:contextmenu="onOpenNodeContextMenu"
|
@open:contextmenu="onOpenNodeContextMenu"
|
||||||
@update="onUpdateNodeParameters"
|
@update="onUpdateNodeParameters"
|
||||||
|
@update:inputs="onUpdateNodeInputs"
|
||||||
|
@update:outputs="onUpdateNodeOutputs"
|
||||||
@move="onUpdateNodePosition"
|
@move="onUpdateNodePosition"
|
||||||
@add="onClickNodeAdd"
|
@add="onClickNodeAdd"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { useCanvas } from '@/composables/useCanvas';
|
||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
import type { EventBus } from 'n8n-design-system';
|
import type { EventBus } from 'n8n-design-system';
|
||||||
import { createEventBus } from 'n8n-design-system';
|
import { createEventBus } from 'n8n-design-system';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
type Props = NodeProps<CanvasNodeData> & {
|
type Props = NodeProps<CanvasNodeData> & {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
@ -47,6 +48,8 @@ const emit = defineEmits<{
|
||||||
activate: [id: string];
|
activate: [id: string];
|
||||||
'open:contextmenu': [id: string, event: MouseEvent, source: 'node-button' | 'node-right-click'];
|
'open:contextmenu': [id: string, event: MouseEvent, source: 'node-button' | 'node-right-click'];
|
||||||
update: [id: string, parameters: Record<string, unknown>];
|
update: [id: string, parameters: Record<string, unknown>];
|
||||||
|
'update:inputs': [id: string];
|
||||||
|
'update:outputs': [id: string];
|
||||||
move: [id: string, position: XYPosition];
|
move: [id: string, position: XYPosition];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -265,6 +268,18 @@ watch(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(inputs, (newValue, oldValue) => {
|
||||||
|
if (!isEqual(newValue, oldValue)) {
|
||||||
|
emit('update:inputs', props.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(outputs, (newValue, oldValue) => {
|
||||||
|
if (!isEqual(newValue, oldValue)) {
|
||||||
|
emit('update:outputs', props.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.eventBus?.on('nodes:action', emitCanvasNodeEvent);
|
props.eventBus?.on('nodes:action', emitCanvasNodeEvent);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1161,13 +1161,14 @@ describe('useCanvasMapping', () => {
|
||||||
expect(mappedConnections.value).toEqual([
|
expect(mappedConnections.value).toEqual([
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: manualTriggerNode.name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: manualTriggerNode.name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
status: undefined,
|
status: undefined,
|
||||||
target: {
|
target: {
|
||||||
|
node: setNode.name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -1249,13 +1250,14 @@ describe('useCanvasMapping', () => {
|
||||||
expect(mappedConnections.value).toEqual([
|
expect(mappedConnections.value).toEqual([
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: manualTriggerNode.name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: manualTriggerNode.name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiTool,
|
type: NodeConnectionType.AiTool,
|
||||||
},
|
},
|
||||||
status: undefined,
|
status: undefined,
|
||||||
target: {
|
target: {
|
||||||
|
node: setNode.name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiTool,
|
type: NodeConnectionType.AiTool,
|
||||||
},
|
},
|
||||||
|
@ -1271,13 +1273,14 @@ describe('useCanvasMapping', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: manualTriggerNode.name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: manualTriggerNode.name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiDocument,
|
type: NodeConnectionType.AiDocument,
|
||||||
},
|
},
|
||||||
status: undefined,
|
status: undefined,
|
||||||
target: {
|
target: {
|
||||||
|
node: setNode.name,
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.AiDocument,
|
type: NodeConnectionType.AiDocument,
|
||||||
},
|
},
|
||||||
|
|
|
@ -614,7 +614,7 @@ export function useCanvasMapping({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectionLabel(connection: CanvasConnection): string {
|
function getConnectionLabel(connection: CanvasConnection): string {
|
||||||
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
|
const fromNode = nodes.value.find((node) => node.name === connection.data?.source.node);
|
||||||
if (!fromNode) {
|
if (!fromNode) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { waitFor } from '@testing-library/vue';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { mockedStore } from '@/__tests__/utils';
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
import {
|
import {
|
||||||
|
AGENT_NODE_TYPE,
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
SET_NODE_TYPE,
|
SET_NODE_TYPE,
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
|
@ -41,6 +42,7 @@ import {
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import { useClipboard } from '@/composables/useClipboard';
|
import { useClipboard } from '@/composables/useClipboard';
|
||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
vi.mock('vue-router', async (importOriginal) => {
|
vi.mock('vue-router', async (importOriginal) => {
|
||||||
const actual = await importOriginal<{}>();
|
const actual = await importOriginal<{}>();
|
||||||
|
@ -1934,6 +1936,304 @@ describe('useCanvasOperations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('revalidateNodeInputConnections', () => {
|
||||||
|
it('should not delete connections when target node does not exist', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nonexistentId = 'nonexistent';
|
||||||
|
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeInputConnections(nonexistentId);
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not delete connections when node type description is not found', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
|
const nodeId = 'test-node';
|
||||||
|
const node = createTestNode({ id: nodeId, type: 'unknown-type' });
|
||||||
|
|
||||||
|
workflowsStore.getNodeById.mockReturnValue(node);
|
||||||
|
nodeTypesStore.getNodeType = () => null;
|
||||||
|
|
||||||
|
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeInputConnections(nodeId);
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove invalid connections that do not match input type', async () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
|
|
||||||
|
workflowsStore.removeConnection = vi.fn();
|
||||||
|
|
||||||
|
const targetNodeId = 'target';
|
||||||
|
const targetNode = createTestNode({
|
||||||
|
id: targetNodeId,
|
||||||
|
name: 'Target Node',
|
||||||
|
type: SET_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const targetNodeType = mockNodeTypeDescription({
|
||||||
|
name: SET_NODE_TYPE,
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceNodeId = 'source';
|
||||||
|
const sourceNode = createTestNode({
|
||||||
|
id: sourceNodeId,
|
||||||
|
name: 'Source Node',
|
||||||
|
type: AGENT_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const sourceNodeType = mockNodeTypeDescription({
|
||||||
|
name: AGENT_NODE_TYPE,
|
||||||
|
outputs: [NodeConnectionType.AiTool],
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[sourceNode.name]: {
|
||||||
|
[NodeConnectionType.AiTool]: [
|
||||||
|
[{ node: targetNode.name, type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
workflowsStore.getNodeById
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode)
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode);
|
||||||
|
|
||||||
|
nodeTypesStore.getNodeType = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(targetNodeType)
|
||||||
|
.mockReturnValueOnce(sourceNodeType);
|
||||||
|
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
|
||||||
|
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeInputConnections(targetNodeId);
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||||||
|
connection: [
|
||||||
|
{ node: sourceNode.name, type: NodeConnectionType.AiTool, index: 0 },
|
||||||
|
{ node: targetNode.name, type: NodeConnectionType.Main, index: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep valid connections that match input type', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
|
|
||||||
|
workflowsStore.removeConnection = vi.fn();
|
||||||
|
|
||||||
|
const targetNodeId = 'target';
|
||||||
|
const targetNode = createTestNode({
|
||||||
|
id: targetNodeId,
|
||||||
|
name: 'Target Node',
|
||||||
|
type: SET_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const targetNodeType = mockNodeTypeDescription({
|
||||||
|
name: SET_NODE_TYPE,
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceNodeId = 'source';
|
||||||
|
const sourceNode = createTestNode({
|
||||||
|
id: sourceNodeId,
|
||||||
|
name: 'Source Node',
|
||||||
|
type: AGENT_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const sourceNodeType = mockNodeTypeDescription({
|
||||||
|
name: AGENT_NODE_TYPE,
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[sourceNode.name]: {
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[{ node: targetNode.name, type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
workflowsStore.getNodeById
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode)
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode);
|
||||||
|
|
||||||
|
nodeTypesStore.getNodeType = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(targetNodeType)
|
||||||
|
.mockReturnValueOnce(sourceNodeType);
|
||||||
|
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
|
||||||
|
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeInputConnections(targetNodeId);
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('revalidateNodeOutputConnections', () => {
|
||||||
|
it('should not delete connections when source node does not exist', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nonexistentId = 'nonexistent';
|
||||||
|
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeOutputConnections(nonexistentId);
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not delete connections when node type description is not found', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
|
const nodeId = 'test-node';
|
||||||
|
const node = createTestNode({ id: nodeId, type: 'unknown-type' });
|
||||||
|
|
||||||
|
workflowsStore.getNodeById.mockReturnValue(node);
|
||||||
|
nodeTypesStore.getNodeType = () => null;
|
||||||
|
|
||||||
|
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeOutputConnections(nodeId);
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove invalid connections that do not match output type', async () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
|
|
||||||
|
workflowsStore.removeConnection = vi.fn();
|
||||||
|
|
||||||
|
const targetNodeId = 'target';
|
||||||
|
const targetNode = createTestNode({
|
||||||
|
id: targetNodeId,
|
||||||
|
name: 'Target Node',
|
||||||
|
type: SET_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const targetNodeType = mockNodeTypeDescription({
|
||||||
|
name: SET_NODE_TYPE,
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceNodeId = 'source';
|
||||||
|
const sourceNode = createTestNode({
|
||||||
|
id: sourceNodeId,
|
||||||
|
name: 'Source Node',
|
||||||
|
type: AGENT_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const sourceNodeType = mockNodeTypeDescription({
|
||||||
|
name: AGENT_NODE_TYPE,
|
||||||
|
outputs: [NodeConnectionType.AiTool],
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[sourceNode.name]: {
|
||||||
|
[NodeConnectionType.AiTool]: [
|
||||||
|
[{ node: targetNode.name, type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
workflowsStore.getNodeById
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode)
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode);
|
||||||
|
|
||||||
|
nodeTypesStore.getNodeType = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(targetNodeType)
|
||||||
|
.mockReturnValueOnce(sourceNodeType);
|
||||||
|
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
|
||||||
|
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeOutputConnections(sourceNodeId);
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||||||
|
connection: [
|
||||||
|
{ node: sourceNode.name, type: NodeConnectionType.AiTool, index: 0 },
|
||||||
|
{ node: targetNode.name, type: NodeConnectionType.Main, index: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep valid connections that match output type', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
|
|
||||||
|
workflowsStore.removeConnection = vi.fn();
|
||||||
|
|
||||||
|
const targetNodeId = 'target';
|
||||||
|
const targetNode = createTestNode({
|
||||||
|
id: targetNodeId,
|
||||||
|
name: 'Target Node',
|
||||||
|
type: SET_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const targetNodeType = mockNodeTypeDescription({
|
||||||
|
name: SET_NODE_TYPE,
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceNodeId = 'source';
|
||||||
|
const sourceNode = createTestNode({
|
||||||
|
id: sourceNodeId,
|
||||||
|
name: 'Source Node',
|
||||||
|
type: AGENT_NODE_TYPE,
|
||||||
|
});
|
||||||
|
const sourceNodeType = mockNodeTypeDescription({
|
||||||
|
name: AGENT_NODE_TYPE,
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[sourceNode.name]: {
|
||||||
|
[NodeConnectionType.AiTool]: [
|
||||||
|
[{ node: targetNode.name, type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
workflowsStore.getNodeById
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode)
|
||||||
|
.mockReturnValueOnce(sourceNode)
|
||||||
|
.mockReturnValueOnce(targetNode);
|
||||||
|
|
||||||
|
nodeTypesStore.getNodeType = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(targetNodeType)
|
||||||
|
.mockReturnValueOnce(sourceNodeType);
|
||||||
|
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
|
||||||
|
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||||
|
revalidateNodeOutputConnections(sourceNodeId);
|
||||||
|
|
||||||
|
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deleteConnectionsByNodeId', () => {
|
describe('deleteConnectionsByNodeId', () => {
|
||||||
it('should delete all connections for a given node ID', () => {
|
it('should delete all connections for a given node ID', () => {
|
||||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
|
|
@ -52,6 +52,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type {
|
import type {
|
||||||
CanvasConnection,
|
CanvasConnection,
|
||||||
CanvasConnectionCreateData,
|
CanvasConnectionCreateData,
|
||||||
|
CanvasConnectionPort,
|
||||||
CanvasNode,
|
CanvasNode,
|
||||||
CanvasNodeMoveEvent,
|
CanvasNodeMoveEvent,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
@ -1230,11 +1231,63 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function revalidateNodeConnections(id: string, connectionMode: CanvasConnectionMode) {
|
||||||
|
const node = workflowsStore.getNodeById(id);
|
||||||
|
const isInput = connectionMode === CanvasConnectionMode.Input;
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
|
if (!nodeType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connections = mapLegacyConnectionsToCanvasConnections(
|
||||||
|
workflowsStore.workflow.connections,
|
||||||
|
workflowsStore.workflow.nodes,
|
||||||
|
);
|
||||||
|
|
||||||
|
connections.forEach((connection) => {
|
||||||
|
const isRelevantConnection = isInput ? connection.target === id : connection.source === id;
|
||||||
|
|
||||||
|
if (isRelevantConnection) {
|
||||||
|
const otherNodeId = isInput ? connection.source : connection.target;
|
||||||
|
|
||||||
|
const otherNode = workflowsStore.getNodeById(otherNodeId);
|
||||||
|
if (!otherNode || !connection.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstNode, secondNode] = isInput ? [otherNode, node] : [node, otherNode];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isConnectionAllowed(
|
||||||
|
firstNode,
|
||||||
|
secondNode,
|
||||||
|
connection.data.source,
|
||||||
|
connection.data.target,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
void nextTick(() => deleteConnection(connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function revalidateNodeInputConnections(id: string) {
|
||||||
|
return revalidateNodeConnections(id, CanvasConnectionMode.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function revalidateNodeOutputConnections(id: string) {
|
||||||
|
return revalidateNodeConnections(id, CanvasConnectionMode.Output);
|
||||||
|
}
|
||||||
|
|
||||||
function isConnectionAllowed(
|
function isConnectionAllowed(
|
||||||
sourceNode: INodeUi,
|
sourceNode: INodeUi,
|
||||||
targetNode: INodeUi,
|
targetNode: INodeUi,
|
||||||
sourceConnection: IConnection,
|
sourceConnection: IConnection | CanvasConnectionPort,
|
||||||
targetConnection: IConnection,
|
targetConnection: IConnection | CanvasConnectionPort,
|
||||||
): boolean {
|
): boolean {
|
||||||
const blocklist = [STICKY_NODE_TYPE];
|
const blocklist = [STICKY_NODE_TYPE];
|
||||||
|
|
||||||
|
@ -1908,6 +1961,8 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
deleteConnection,
|
deleteConnection,
|
||||||
revertDeleteConnection,
|
revertDeleteConnection,
|
||||||
deleteConnectionsByNodeId,
|
deleteConnectionsByNodeId,
|
||||||
|
revalidateNodeInputConnections,
|
||||||
|
revalidateNodeOutputConnections,
|
||||||
isConnectionAllowed,
|
isConnectionAllowed,
|
||||||
filterConnectionsByNodes,
|
filterConnectionsByNodes,
|
||||||
importWorkflowData,
|
importWorkflowData,
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
||||||
import type {
|
import type { ExecutionStatus, INodeConnections, NodeConnectionType } from 'n8n-workflow';
|
||||||
ExecutionStatus,
|
|
||||||
INodeConnections,
|
|
||||||
IConnection,
|
|
||||||
NodeConnectionType,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import type {
|
import type {
|
||||||
DefaultEdge,
|
DefaultEdge,
|
||||||
Node,
|
Node,
|
||||||
|
@ -15,11 +10,8 @@ import type {
|
||||||
} from '@vue-flow/core';
|
} from '@vue-flow/core';
|
||||||
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
import type { PartialBy } from '@/utils/typeHelpers';
|
|
||||||
import type { EventBus } from 'n8n-design-system';
|
import type { EventBus } from 'n8n-design-system';
|
||||||
|
|
||||||
export type CanvasConnectionPortType = NodeConnectionType;
|
|
||||||
|
|
||||||
export const enum CanvasConnectionMode {
|
export const enum CanvasConnectionMode {
|
||||||
Input = 'inputs',
|
Input = 'inputs',
|
||||||
Output = 'outputs',
|
Output = 'outputs',
|
||||||
|
@ -31,10 +23,11 @@ export const canvasConnectionModes = [
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type CanvasConnectionPort = {
|
export type CanvasConnectionPort = {
|
||||||
type: CanvasConnectionPortType;
|
node?: string;
|
||||||
|
type: NodeConnectionType;
|
||||||
|
index: number;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
maxConnections?: number;
|
maxConnections?: number;
|
||||||
index: number;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,7 +117,6 @@ export type CanvasNode = Node<CanvasNodeData>;
|
||||||
export interface CanvasConnectionData {
|
export interface CanvasConnectionData {
|
||||||
source: CanvasConnectionPort;
|
source: CanvasConnectionPort;
|
||||||
target: CanvasConnectionPort;
|
target: CanvasConnectionPort;
|
||||||
fromNodeName?: string;
|
|
||||||
status?: 'success' | 'error' | 'pinned' | 'running';
|
status?: 'success' | 'error' | 'pinned' | 'running';
|
||||||
maxConnections?: number;
|
maxConnections?: number;
|
||||||
}
|
}
|
||||||
|
@ -137,8 +129,8 @@ export type CanvasConnectionCreateData = {
|
||||||
target: string;
|
target: string;
|
||||||
targetHandle: string;
|
targetHandle: string;
|
||||||
data: {
|
data: {
|
||||||
source: PartialBy<IConnection, 'node'>;
|
source: CanvasConnectionPort;
|
||||||
target: PartialBy<IConnection, 'node'>;
|
target: CanvasConnectionPort;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -77,12 +77,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
targetHandle,
|
targetHandle,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -215,12 +216,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle: sourceHandleA,
|
sourceHandle: sourceHandleA,
|
||||||
targetHandle: targetHandleA,
|
targetHandle: targetHandleA,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -233,12 +235,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle: sourceHandleB,
|
sourceHandle: sourceHandleB,
|
||||||
targetHandle: targetHandleB,
|
targetHandle: targetHandleB,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -334,12 +337,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle: sourceHandleA,
|
sourceHandle: sourceHandleA,
|
||||||
targetHandle: targetHandleA,
|
targetHandle: targetHandleA,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -352,12 +356,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle: sourceHandleB,
|
sourceHandle: sourceHandleB,
|
||||||
targetHandle: targetHandleB,
|
targetHandle: targetHandleB,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[2].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -475,12 +480,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle: sourceHandleA,
|
sourceHandle: sourceHandleA,
|
||||||
targetHandle: targetHandleA,
|
targetHandle: targetHandleA,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -493,12 +499,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle: sourceHandleB,
|
sourceHandle: sourceHandleB,
|
||||||
targetHandle: targetHandleB,
|
targetHandle: targetHandleB,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiMemory,
|
type: NodeConnectionType.AiMemory,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[2].name,
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.AiMemory,
|
type: NodeConnectionType.AiMemory,
|
||||||
},
|
},
|
||||||
|
@ -511,12 +518,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle: sourceHandleC,
|
sourceHandle: sourceHandleC,
|
||||||
targetHandle: targetHandleC,
|
targetHandle: targetHandleC,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[1].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[2].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -587,12 +595,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
targetHandle,
|
targetHandle,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
@ -662,12 +671,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
targetHandle,
|
targetHandle,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: nodes[0].name,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: nodes[0].name,
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: nodes[1].name,
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,7 +22,8 @@ export function mapLegacyConnectionsToCanvasConnections(
|
||||||
const fromPorts = legacyConnections[fromNodeName][fromConnectionType];
|
const fromPorts = legacyConnections[fromNodeName][fromConnectionType];
|
||||||
fromPorts?.forEach((toPorts, fromIndex) => {
|
fromPorts?.forEach((toPorts, fromIndex) => {
|
||||||
toPorts?.forEach((toPort) => {
|
toPorts?.forEach((toPort) => {
|
||||||
const toId = nodes.find((node) => node.name === toPort.node)?.id ?? '';
|
const toNodeName = toPort.node;
|
||||||
|
const toId = nodes.find((node) => node.name === toNodeName)?.id ?? '';
|
||||||
const toConnectionType = toPort.type as NodeConnectionType;
|
const toConnectionType = toPort.type as NodeConnectionType;
|
||||||
const toIndex = toPort.index;
|
const toIndex = toPort.index;
|
||||||
|
|
||||||
|
@ -53,12 +54,13 @@ export function mapLegacyConnectionsToCanvasConnections(
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
targetHandle,
|
targetHandle,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName,
|
|
||||||
source: {
|
source: {
|
||||||
|
node: fromNodeName,
|
||||||
index: fromIndex,
|
index: fromIndex,
|
||||||
type: fromConnectionType,
|
type: fromConnectionType,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
|
node: toNodeName,
|
||||||
index: toIndex,
|
index: toIndex,
|
||||||
type: toConnectionType,
|
type: toConnectionType,
|
||||||
},
|
},
|
||||||
|
|
|
@ -180,6 +180,8 @@ const {
|
||||||
revertCreateConnection,
|
revertCreateConnection,
|
||||||
deleteConnection,
|
deleteConnection,
|
||||||
revertDeleteConnection,
|
revertDeleteConnection,
|
||||||
|
revalidateNodeInputConnections,
|
||||||
|
revalidateNodeOutputConnections,
|
||||||
setNodeActiveByName,
|
setNodeActiveByName,
|
||||||
addConnections,
|
addConnections,
|
||||||
importWorkflowData,
|
importWorkflowData,
|
||||||
|
@ -723,6 +725,14 @@ function onUpdateNodeParameters(id: string, parameters: Record<string, unknown>)
|
||||||
setNodeParameters(id, parameters);
|
setNodeParameters(id, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUpdateNodeInputs(id: string) {
|
||||||
|
revalidateNodeInputConnections(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdateNodeOutputs(id: string) {
|
||||||
|
revalidateNodeOutputConnections(id);
|
||||||
|
}
|
||||||
|
|
||||||
function onClickNodeAdd(source: string, sourceHandle: string) {
|
function onClickNodeAdd(source: string, sourceHandle: string) {
|
||||||
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
||||||
connection: {
|
connection: {
|
||||||
|
@ -1618,6 +1628,8 @@ onBeforeUnmount(() => {
|
||||||
@update:node:enabled="onToggleNodeDisabled"
|
@update:node:enabled="onToggleNodeDisabled"
|
||||||
@update:node:name="onOpenRenameNodeModal"
|
@update:node:name="onOpenRenameNodeModal"
|
||||||
@update:node:parameters="onUpdateNodeParameters"
|
@update:node:parameters="onUpdateNodeParameters"
|
||||||
|
@update:node:inputs="onUpdateNodeInputs"
|
||||||
|
@update:node:outputs="onUpdateNodeOutputs"
|
||||||
@click:node:add="onClickNodeAdd"
|
@click:node:add="onClickNodeAdd"
|
||||||
@run:node="onRunWorkflowToNode"
|
@run:node="onRunWorkflowToNode"
|
||||||
@delete:node="onDeleteNode"
|
@delete:node="onDeleteNode"
|
||||||
|
|
Loading…
Reference in a new issue