feat(editor): Add connection port validation on new canvas (no-changelog) (#11342)

This commit is contained in:
Alex Grozav 2024-10-22 15:14:12 +03:00 committed by GitHub
parent d04860d19e
commit 5fae187a0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 223 additions and 81 deletions

View file

@ -1133,6 +1133,11 @@ describe('useCanvasOperations', () => {
name: sourceNode.type, name: sourceNode.type,
outputs: [], outputs: [],
}); });
const sourceHandle: IConnection = {
node: sourceNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const targetNode = mockNode({ const targetNode = mockNode({
id: '2', id: '2',
type: 'targetType', type: 'targetType',
@ -1142,6 +1147,11 @@ describe('useCanvasOperations', () => {
name: targetNode.type, name: targetNode.type,
inputs: [], inputs: [],
}); });
const targetHandle: IConnection = {
node: targetNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1155,14 +1165,7 @@ describe('useCanvasOperations', () => {
); );
const { isConnectionAllowed } = useCanvasOperations({ router }); const { isConnectionAllowed } = useCanvasOperations({ router });
expect( expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).toBe(false);
}); });
it('should return false if target node does not exist in the workflow', () => { it('should return false if target node does not exist in the workflow', () => {
@ -1178,6 +1181,11 @@ describe('useCanvasOperations', () => {
name: sourceNode.type, name: sourceNode.type,
outputs: [], outputs: [],
}); });
const sourceHandle: IConnection = {
node: sourceNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const targetNode = mockNode({ const targetNode = mockNode({
id: '2', id: '2',
type: 'targetType', type: 'targetType',
@ -1187,6 +1195,11 @@ describe('useCanvasOperations', () => {
name: targetNode.type, name: targetNode.type,
inputs: [NodeConnectionType.Main], inputs: [NodeConnectionType.Main],
}); });
const targetHandle: IConnection = {
node: targetNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1199,14 +1212,7 @@ describe('useCanvasOperations', () => {
); );
const { isConnectionAllowed } = useCanvasOperations({ router }); const { isConnectionAllowed } = useCanvasOperations({ router });
expect( expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).toBe(false);
}); });
it('should return false if source node does not have connection type', () => { it('should return false if source node does not have connection type', () => {
@ -1221,6 +1227,11 @@ describe('useCanvasOperations', () => {
name: sourceNode.type, name: sourceNode.type,
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],
}); });
const sourceHandle: IConnection = {
node: sourceNode.name,
type: NodeConnectionType.AiTool,
index: 0,
};
const targetNode = mockNode({ const targetNode = mockNode({
id: '2', id: '2',
@ -1231,6 +1242,11 @@ describe('useCanvasOperations', () => {
name: 'targetType', name: 'targetType',
inputs: [NodeConnectionType.AiTool], inputs: [NodeConnectionType.AiTool],
}); });
const targetHandle: IConnection = {
node: targetNode.name,
type: NodeConnectionType.AiTool,
index: 0,
};
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1247,14 +1263,7 @@ describe('useCanvasOperations', () => {
})[nodeTypeName], })[nodeTypeName],
); );
expect( expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.AiTool,
NodeConnectionType.AiTool,
),
).toBe(false);
}); });
it('should return false if target node does not have connection type', () => { it('should return false if target node does not have connection type', () => {
@ -1269,6 +1278,11 @@ describe('useCanvasOperations', () => {
name: sourceNode.type, name: sourceNode.type,
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],
}); });
const sourceHandle: IConnection = {
node: sourceNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const targetNode = mockNode({ const targetNode = mockNode({
id: '2', id: '2',
@ -1279,6 +1293,11 @@ describe('useCanvasOperations', () => {
name: 'targetType', name: 'targetType',
inputs: [NodeConnectionType.AiTool], inputs: [NodeConnectionType.AiTool],
}); });
const targetHandle: IConnection = {
node: targetNode.name,
type: NodeConnectionType.AiTool,
index: 0,
};
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1295,14 +1314,7 @@ describe('useCanvasOperations', () => {
})[nodeTypeName], })[nodeTypeName],
); );
expect( expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.AiTool,
),
).toBe(false);
}); });
it('should return false if source node type is not allowed by target node input filter', () => { 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, name: sourceNode.type,
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],
}); });
const sourceHandle: IConnection = {
node: sourceNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const targetNode = mockNode({ const targetNode = mockNode({
id: '2', id: '2',
@ -1336,6 +1353,11 @@ describe('useCanvasOperations', () => {
}, },
], ],
}); });
const targetHandle: IConnection = {
node: targetNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1352,20 +1374,12 @@ describe('useCanvasOperations', () => {
})[nodeTypeName], })[nodeTypeName],
); );
expect( expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).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 workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore); const nodeTypesStore = mockedStore(useNodeTypesStore);
const sourceNode = mockNode({ const sourceNode = mockNode({
id: '1', id: '1',
type: 'sourceType', type: 'sourceType',
@ -1376,6 +1390,11 @@ describe('useCanvasOperations', () => {
name: sourceNode.type, name: sourceNode.type,
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],
}); });
const sourceHandle: IConnection = {
node: sourceNode.name,
type: NodeConnectionType.Main,
index: 1,
};
const targetNode = mockNode({ const targetNode = mockNode({
id: '2', id: '2',
@ -1394,6 +1413,11 @@ describe('useCanvasOperations', () => {
}, },
], ],
}); });
const targetHandle: IConnection = {
node: targetNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1410,14 +1434,128 @@ describe('useCanvasOperations', () => {
})[nodeTypeName], })[nodeTypeName],
); );
expect( expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
isConnectionAllowed( });
sourceNode,
targetNode, it('should return false if target node type does not have connection type index', () => {
NodeConnectionType.Main, const workflowsStore = mockedStore(useWorkflowsStore);
NodeConnectionType.Main, const nodeTypesStore = mockedStore(useNodeTypesStore);
), const sourceNode = mockNode({
).toBe(true); 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', () => { it('should return true if all conditions are met and no filter is set', () => {
@ -1434,6 +1572,11 @@ describe('useCanvasOperations', () => {
name: sourceNode.type, name: sourceNode.type,
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],
}); });
const sourceHandle: IConnection = {
node: sourceNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const targetNode = mockNode({ const targetNode = mockNode({
id: '2', id: '2',
@ -1449,6 +1592,11 @@ describe('useCanvasOperations', () => {
}, },
], ],
}); });
const targetHandle: IConnection = {
node: targetNode.name,
type: NodeConnectionType.Main,
index: 0,
};
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1465,14 +1613,7 @@ describe('useCanvasOperations', () => {
})[nodeTypeName], })[nodeTypeName],
); );
expect( expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).toBe(true);
}); });
it('should return true if node connecting to itself', () => { it('should return true if node connecting to itself', () => {
@ -1489,6 +1630,16 @@ describe('useCanvasOperations', () => {
name: sourceNode.type, name: sourceNode.type,
outputs: [NodeConnectionType.Main], 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); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
@ -1503,14 +1654,7 @@ describe('useCanvasOperations', () => {
})[nodeTypeName], })[nodeTypeName],
); );
expect( expect(isConnectionAllowed(sourceNode, sourceNode, sourceHandle, targetHandle)).toBe(true);
isConnectionAllowed(
sourceNode,
sourceNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).toBe(true);
}); });
}); });

View file

@ -1154,14 +1154,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
connection, connection,
); );
if ( if (!isConnectionAllowed(sourceNode, targetNode, mappedConnection[0], mappedConnection[1])) {
!isConnectionAllowed(
sourceNode,
targetNode,
mappedConnection[0].type,
mappedConnection[1].type,
)
) {
return; return;
} }
@ -1298,12 +1291,12 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
function isConnectionAllowed( function isConnectionAllowed(
sourceNode: INodeUi, sourceNode: INodeUi,
targetNode: INodeUi, targetNode: INodeUi,
sourceConnectionType: NodeConnectionType, sourceConnection: IConnection,
targetConnectionType: NodeConnectionType, targetConnection: IConnection,
): boolean { ): boolean {
const blocklist = [STICKY_NODE_TYPE]; const blocklist = [STICKY_NODE_TYPE];
if (sourceConnectionType !== targetConnectionType) { if (sourceConnection.type !== targetConnection.type) {
return false; return false;
} }
@ -1329,10 +1322,13 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
const sourceNodeHasOutputConnectionOfType = !!sourceNodeOutputs.find((output) => { const sourceNodeHasOutputConnectionOfType = !!sourceNodeOutputs.find((output) => {
const outputType = typeof output === 'string' ? output : output.type; 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; return false;
} }
@ -1354,7 +1350,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
const targetNodeHasInputConnectionOfType = !!targetNodeInputs.find((input) => { const targetNodeHasInputConnectionOfType = !!targetNodeInputs.find((input) => {
const inputType = typeof input === 'string' ? input : input.type; 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; const filter = typeof input === 'object' && 'filter' in input ? input.filter : undefined;
if (filter?.nodes.length && !filter.nodes.includes(sourceNode.type)) { if (filter?.nodes.length && !filter.nodes.includes(sourceNode.type)) {
@ -1373,7 +1369,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
return true; return true;
}); });
return targetNodeHasInputConnectionOfType; const targetNodeHasInputConnectionPortOfType = targetConnection.index < targetNodeInputs.length;
return targetNodeHasInputConnectionOfType && targetNodeHasInputConnectionPortOfType;
} }
function addConnections( function addConnections(