mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(editor): Remove fallback model connection when disabled in root node (#23089)
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
EXECUTABLE_TRIGGER_NODE_TYPES,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
NO_OP_NODE_TYPE,
|
||||
OPEN_AI_CHAT_MODEL_NODE_TYPE,
|
||||
SET_NODE_TYPE,
|
||||
SIMULATE_NODE_TYPE,
|
||||
STICKY_NODE_TYPE,
|
||||
@@ -121,6 +122,7 @@ export const mockNodes = [
|
||||
mockNode({ name: 'Chat Trigger', type: CHAT_TRIGGER_NODE_TYPE }),
|
||||
mockNode({ name: 'Form Trigger', type: FORM_TRIGGER_NODE_TYPE }),
|
||||
mockNode({ name: 'Agent', type: AGENT_NODE_TYPE }),
|
||||
mockNode({ name: 'OpenAI Model', type: OPEN_AI_CHAT_MODEL_NODE_TYPE }),
|
||||
mockNode({ name: 'Sticky', type: STICKY_NODE_TYPE }),
|
||||
mockNode({ name: 'Simulate', type: SIMULATE_NODE_TYPE }),
|
||||
mockNode({ name: CanvasNodeRenderType.AddNodes, type: CanvasNodeRenderType.AddNodes }),
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
AGENT_NODE_TYPE,
|
||||
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
OPEN_AI_CHAT_MODEL_NODE_TYPE,
|
||||
SET_NODE_TYPE,
|
||||
STICKY_NODE_TYPE,
|
||||
VIEWS,
|
||||
@@ -2874,6 +2875,134 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove connections if the input port index is no longer valid for the 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: AGENT_NODE_TYPE,
|
||||
});
|
||||
const targetNodeType = mockNodeTypeDescription({
|
||||
name: AGENT_NODE_TYPE,
|
||||
inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.AiLanguageModel],
|
||||
});
|
||||
|
||||
const sourceNodeId = 'source';
|
||||
const sourceNode = createTestNode({
|
||||
id: sourceNodeId,
|
||||
name: 'Source Node',
|
||||
type: OPEN_AI_CHAT_MODEL_NODE_TYPE,
|
||||
});
|
||||
const sourceNodeType = mockNodeTypeDescription({
|
||||
name: OPEN_AI_CHAT_MODEL_NODE_TYPE,
|
||||
outputs: [NodeConnectionTypes.AiLanguageModel],
|
||||
});
|
||||
|
||||
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||||
workflowsStore.workflow.connections = {
|
||||
[sourceNode.name]: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
workflowsStore.getNodeById.mockImplementation((id) => {
|
||||
if (id === sourceNodeId) return sourceNode;
|
||||
if (id === targetNodeId) return targetNode;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
nodeTypesStore.getNodeType = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(targetNodeType)
|
||||
.mockReturnValueOnce(sourceNodeType);
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.workflowObject = workflowObject;
|
||||
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations();
|
||||
revalidateNodeInputConnections(targetNodeId);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||||
connection: [
|
||||
{ node: sourceNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 0 },
|
||||
{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep connections if the input port index is still valid for the 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: AGENT_NODE_TYPE,
|
||||
});
|
||||
const targetNodeType = mockNodeTypeDescription({
|
||||
name: AGENT_NODE_TYPE,
|
||||
inputs: [
|
||||
NodeConnectionTypes.Main,
|
||||
NodeConnectionTypes.AiLanguageModel,
|
||||
NodeConnectionTypes.AiLanguageModel,
|
||||
],
|
||||
});
|
||||
|
||||
const sourceNodeId = 'source';
|
||||
const sourceNode = createTestNode({
|
||||
id: sourceNodeId,
|
||||
name: 'Source Node',
|
||||
type: OPEN_AI_CHAT_MODEL_NODE_TYPE,
|
||||
});
|
||||
const sourceNodeType = mockNodeTypeDescription({
|
||||
name: OPEN_AI_CHAT_MODEL_NODE_TYPE,
|
||||
outputs: [NodeConnectionTypes.AiLanguageModel],
|
||||
});
|
||||
|
||||
workflowsStore.workflow.nodes = [sourceNode, targetNode];
|
||||
workflowsStore.workflow.connections = {
|
||||
[sourceNode.name]: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
workflowsStore.getNodeById.mockImplementation((id) => {
|
||||
if (id === sourceNodeId) return sourceNode;
|
||||
if (id === targetNodeId) return targetNode;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
nodeTypesStore.getNodeType = vi.fn().mockImplementation((type) => {
|
||||
if (type === AGENT_NODE_TYPE) return targetNodeType;
|
||||
if (type === OPEN_AI_CHAT_MODEL_NODE_TYPE) return sourceNodeType;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.workflowObject = workflowObject;
|
||||
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations();
|
||||
revalidateNodeInputConnections(targetNodeId);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('revalidateNodeOutputConnections', () => {
|
||||
|
||||
@@ -1589,6 +1589,25 @@ export function useCanvasOperations() {
|
||||
);
|
||||
};
|
||||
|
||||
const filterConnectionsByType = (
|
||||
connections: Array<NodeConnectionType | INodeInputConfiguration | INodeOutputConfiguration>,
|
||||
type: NodeConnectionType,
|
||||
) =>
|
||||
connections.filter((connection) => {
|
||||
const connectionType = typeof connection === 'string' ? connection : connection.type;
|
||||
return connectionType === type;
|
||||
});
|
||||
|
||||
const getInputFilter = (
|
||||
connection?: NodeConnectionType | INodeInputConfiguration | INodeOutputConfiguration,
|
||||
) => {
|
||||
if (connection && typeof connection === 'object' && 'filter' in connection) {
|
||||
return connection.filter;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
if (sourceConnection.type !== targetConnection.type) {
|
||||
return false;
|
||||
}
|
||||
@@ -1613,13 +1632,11 @@ export function useCanvasOperations() {
|
||||
) || [];
|
||||
}
|
||||
|
||||
const sourceNodeHasOutputConnectionOfType = !!sourceNodeOutputs.find((output) => {
|
||||
const outputType = typeof output === 'string' ? output : output.type;
|
||||
return outputType === sourceConnection.type;
|
||||
});
|
||||
const sourceOutputsOfType = filterConnectionsByType(sourceNodeOutputs, sourceConnection.type);
|
||||
const sourceNodeHasOutputConnectionOfType = sourceOutputsOfType.length > 0;
|
||||
|
||||
const sourceNodeHasOutputConnectionPortOfType =
|
||||
sourceConnection.index < sourceNodeOutputs.length;
|
||||
sourceConnection.index < sourceOutputsOfType.length;
|
||||
|
||||
const isMissingOutputConnection =
|
||||
!sourceNodeHasOutputConnectionOfType || !sourceNodeHasOutputConnectionPortOfType;
|
||||
@@ -1644,31 +1661,33 @@ export function useCanvasOperations() {
|
||||
) || [];
|
||||
}
|
||||
|
||||
const targetNodeHasInputConnectionOfType = !!targetNodeInputs.find((input) => {
|
||||
const inputType = typeof input === 'string' ? input : input.type;
|
||||
if (inputType !== targetConnection.type) return false;
|
||||
const targetInputsOfType = filterConnectionsByType(targetNodeInputs, targetConnection.type);
|
||||
const targetNodeHasInputConnectionOfType = targetInputsOfType.length > 0;
|
||||
const targetNodeHasInputConnectionPortOfType =
|
||||
targetConnection.index < targetInputsOfType.length;
|
||||
|
||||
const filter = typeof input === 'object' && 'filter' in input ? input.filter : undefined;
|
||||
if (
|
||||
(filter?.nodes?.length && !filter.nodes?.includes(sourceNode.type)) ||
|
||||
(filter?.excludedNodes?.length && filter.excludedNodes?.includes(sourceNode.type))
|
||||
) {
|
||||
toast.showToast({
|
||||
title: i18n.baseText('nodeView.showError.nodeNodeCompatible.title'),
|
||||
message: i18n.baseText('nodeView.showError.nodeNodeCompatible.message', {
|
||||
interpolate: { sourceNodeName: sourceNode.name, targetNodeName: targetNode.name },
|
||||
}),
|
||||
type: 'error',
|
||||
duration: 5000,
|
||||
});
|
||||
const targetConnectionDefinition = targetNodeHasInputConnectionPortOfType
|
||||
? targetInputsOfType[targetConnection.index]
|
||||
: undefined;
|
||||
const targetConnectionFilter = getInputFilter(targetConnectionDefinition);
|
||||
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(targetConnectionFilter?.nodes?.length &&
|
||||
!targetConnectionFilter.nodes.includes(sourceNode.type)) ||
|
||||
(targetConnectionFilter?.excludedNodes?.length &&
|
||||
targetConnectionFilter.excludedNodes.includes(sourceNode.type))
|
||||
) {
|
||||
toast.showToast({
|
||||
title: i18n.baseText('nodeView.showError.nodeNodeCompatible.title'),
|
||||
message: i18n.baseText('nodeView.showError.nodeNodeCompatible.message', {
|
||||
interpolate: { sourceNodeName: sourceNode.name, targetNodeName: targetNode.name },
|
||||
}),
|
||||
type: 'error',
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const targetNodeHasInputConnectionPortOfType = targetConnection.index < targetNodeInputs.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
const isMissingInputConnection =
|
||||
!targetNodeHasInputConnectionOfType || !targetNodeHasInputConnectionPortOfType;
|
||||
|
||||
@@ -36,6 +36,7 @@ export const MCP_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.mcpTrigger';
|
||||
export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger';
|
||||
export const CHAT_NODE_TYPE = '@n8n/n8n-nodes-langchain.chat';
|
||||
export const AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
|
||||
export const OPEN_AI_CHAT_MODEL_NODE_TYPE = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
||||
export const OPEN_AI_NODE_TYPE = '@n8n/n8n-nodes-langchain.openAi';
|
||||
export const OPEN_AI_NODE_MESSAGE_ASSISTANT_TYPE =
|
||||
'@n8n/n8n-nodes-langchain.openAi.assistant.message';
|
||||
|
||||
Reference in New Issue
Block a user