mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
feat(editor): Add connection port validation on new canvas (no-changelog) (#11342)
This commit is contained in:
parent
d04860d19e
commit
5fae187a0a
|
@ -1133,6 +1133,11 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
type: 'targetType',
|
||||
|
@ -1142,6 +1147,11 @@ describe('useCanvasOperations', () => {
|
|||
name: targetNode.type,
|
||||
inputs: [],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1155,14 +1165,7 @@ describe('useCanvasOperations', () => {
|
|||
);
|
||||
|
||||
const { isConnectionAllowed } = useCanvasOperations({ router });
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
NodeConnectionType.Main,
|
||||
NodeConnectionType.Main,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if target node does not exist in the workflow', () => {
|
||||
|
@ -1178,6 +1181,11 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
type: 'targetType',
|
||||
|
@ -1187,6 +1195,11 @@ describe('useCanvasOperations', () => {
|
|||
name: targetNode.type,
|
||||
inputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1199,14 +1212,7 @@ describe('useCanvasOperations', () => {
|
|||
);
|
||||
|
||||
const { isConnectionAllowed } = useCanvasOperations({ router });
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
NodeConnectionType.Main,
|
||||
NodeConnectionType.Main,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if source node does not have connection type', () => {
|
||||
|
@ -1221,6 +1227,11 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.AiTool,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
|
@ -1231,6 +1242,11 @@ describe('useCanvasOperations', () => {
|
|||
name: 'targetType',
|
||||
inputs: [NodeConnectionType.AiTool],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.AiTool,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1247,14 +1263,7 @@ describe('useCanvasOperations', () => {
|
|||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
NodeConnectionType.AiTool,
|
||||
NodeConnectionType.AiTool,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if target node does not have connection type', () => {
|
||||
|
@ -1269,6 +1278,11 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
|
@ -1279,6 +1293,11 @@ describe('useCanvasOperations', () => {
|
|||
name: 'targetType',
|
||||
inputs: [NodeConnectionType.AiTool],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.AiTool,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1295,14 +1314,7 @@ describe('useCanvasOperations', () => {
|
|||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
NodeConnectionType.Main,
|
||||
NodeConnectionType.AiTool,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if source node type is not allowed by target node input filter', () => {
|
||||
|
@ -1318,6 +1330,11 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
|
@ -1336,6 +1353,11 @@ describe('useCanvasOperations', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1352,20 +1374,12 @@ describe('useCanvasOperations', () => {
|
|||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
NodeConnectionType.Main,
|
||||
NodeConnectionType.Main,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if all conditions including filter are met', () => {
|
||||
it('should return false if source node type does not have connection type index', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
|
||||
const sourceNode = mockNode({
|
||||
id: '1',
|
||||
type: 'sourceType',
|
||||
|
@ -1376,6 +1390,11 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
|
@ -1394,6 +1413,11 @@ describe('useCanvasOperations', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1410,14 +1434,128 @@ describe('useCanvasOperations', () => {
|
|||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
NodeConnectionType.Main,
|
||||
NodeConnectionType.Main,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if target node type does not have connection type index', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
const sourceNode = mockNode({
|
||||
id: '1',
|
||||
type: 'sourceType',
|
||||
name: 'Source Node',
|
||||
typeVersion: 1,
|
||||
});
|
||||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
type: 'targetType',
|
||||
name: 'Target Node',
|
||||
typeVersion: 1,
|
||||
});
|
||||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||||
name: targetNode.type,
|
||||
inputs: [
|
||||
{
|
||||
type: NodeConnectionType.Main,
|
||||
filter: {
|
||||
nodes: [sourceNode.type],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
nodeTypesStore.getNodeType = vi.fn(
|
||||
(nodeTypeName: string) =>
|
||||
({
|
||||
[sourceNode.type]: sourceNodeTypeDescription,
|
||||
[targetNode.type]: targetNodeTypeDescription,
|
||||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if all conditions including filter are met', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
|
||||
const sourceNode = mockNode({
|
||||
id: '1',
|
||||
type: 'sourceType',
|
||||
name: 'Source Node',
|
||||
typeVersion: 1,
|
||||
});
|
||||
const sourceNodeTypeDescription = mockNodeTypeDescription({
|
||||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
type: 'targetType',
|
||||
name: 'Target Node',
|
||||
typeVersion: 1,
|
||||
});
|
||||
const targetNodeTypeDescription = mockNodeTypeDescription({
|
||||
name: targetNode.type,
|
||||
inputs: [
|
||||
{
|
||||
type: NodeConnectionType.Main,
|
||||
filter: {
|
||||
nodes: [sourceNode.type],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
nodeTypesStore.getNodeType = vi.fn(
|
||||
(nodeTypeName: string) =>
|
||||
({
|
||||
[sourceNode.type]: sourceNodeTypeDescription,
|
||||
[targetNode.type]: targetNodeTypeDescription,
|
||||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if all conditions are met and no filter is set', () => {
|
||||
|
@ -1434,6 +1572,11 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const targetNode = mockNode({
|
||||
id: '2',
|
||||
|
@ -1449,6 +1592,11 @@ describe('useCanvasOperations', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const targetHandle: IConnection = {
|
||||
node: targetNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1465,14 +1613,7 @@ describe('useCanvasOperations', () => {
|
|||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
NodeConnectionType.Main,
|
||||
NodeConnectionType.Main,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if node connecting to itself', () => {
|
||||
|
@ -1489,6 +1630,16 @@ describe('useCanvasOperations', () => {
|
|||
name: sourceNode.type,
|
||||
outputs: [NodeConnectionType.Main],
|
||||
});
|
||||
const sourceHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
const targetHandle: IConnection = {
|
||||
node: sourceNode.name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
@ -1503,14 +1654,7 @@ describe('useCanvasOperations', () => {
|
|||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
expect(
|
||||
isConnectionAllowed(
|
||||
sourceNode,
|
||||
sourceNode,
|
||||
NodeConnectionType.Main,
|
||||
NodeConnectionType.Main,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isConnectionAllowed(sourceNode, sourceNode, sourceHandle, targetHandle)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1154,14 +1154,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
|||
connection,
|
||||
);
|
||||
|
||||
if (
|
||||
!isConnectionAllowed(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
mappedConnection[0].type,
|
||||
mappedConnection[1].type,
|
||||
)
|
||||
) {
|
||||
if (!isConnectionAllowed(sourceNode, targetNode, mappedConnection[0], mappedConnection[1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1298,12 +1291,12 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
|||
function isConnectionAllowed(
|
||||
sourceNode: INodeUi,
|
||||
targetNode: INodeUi,
|
||||
sourceConnectionType: NodeConnectionType,
|
||||
targetConnectionType: NodeConnectionType,
|
||||
sourceConnection: IConnection,
|
||||
targetConnection: IConnection,
|
||||
): boolean {
|
||||
const blocklist = [STICKY_NODE_TYPE];
|
||||
|
||||
if (sourceConnectionType !== targetConnectionType) {
|
||||
if (sourceConnection.type !== targetConnection.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1329,10 +1322,13 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
|||
|
||||
const sourceNodeHasOutputConnectionOfType = !!sourceNodeOutputs.find((output) => {
|
||||
const outputType = typeof output === 'string' ? output : output.type;
|
||||
return outputType === sourceConnectionType;
|
||||
return outputType === sourceConnection.type;
|
||||
});
|
||||
|
||||
if (!sourceNodeHasOutputConnectionOfType) {
|
||||
const sourceNodeHasOutputConnectionPortOfType =
|
||||
sourceConnection.index < sourceNodeOutputs.length;
|
||||
|
||||
if (!sourceNodeHasOutputConnectionOfType || !sourceNodeHasOutputConnectionPortOfType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1354,7 +1350,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
|||
|
||||
const targetNodeHasInputConnectionOfType = !!targetNodeInputs.find((input) => {
|
||||
const inputType = typeof input === 'string' ? input : input.type;
|
||||
if (inputType !== targetConnectionType) return false;
|
||||
if (inputType !== targetConnection.type) return false;
|
||||
|
||||
const filter = typeof input === 'object' && 'filter' in input ? input.filter : undefined;
|
||||
if (filter?.nodes.length && !filter.nodes.includes(sourceNode.type)) {
|
||||
|
@ -1373,7 +1369,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
|||
return true;
|
||||
});
|
||||
|
||||
return targetNodeHasInputConnectionOfType;
|
||||
const targetNodeHasInputConnectionPortOfType = targetConnection.index < targetNodeInputs.length;
|
||||
|
||||
return targetNodeHasInputConnectionOfType && targetNodeHasInputConnectionPortOfType;
|
||||
}
|
||||
|
||||
function addConnections(
|
||||
|
|
Loading…
Reference in a new issue