fix(editor): Update new canvas connection checks (no-changelog) (#11019)

This commit is contained in:
Alex Grozav 2024-09-30 17:09:17 +03:00 committed by GitHub
parent b5f4afe12e
commit 805a1140c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 304 additions and 115 deletions

View file

@ -38,7 +38,7 @@ const runDataLabel = computed(() =>
const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value);
const plusStatus = computed(() => (runData.value ? 'success' : 'default'));
const plusType = computed(() => (runData.value ? 'success' : 'default'));
const plusLineSize = computed(
() =>
@ -73,7 +73,7 @@ function onClickAdd() {
data-test-id="canvas-handle-plus"
:line-size="plusLineSize"
:handle-classes="handleClasses"
:status="plusStatus"
:type="plusType"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@click:plus="onClickAdd"

View file

@ -30,7 +30,7 @@ const isHandlePlusVisible = computed(
() => !isConnecting.value || isHovered.value || supportsMultipleConnections.value,
);
const plusStatus = computed(() => (runData.value ? 'success' : 'ai'));
const plusType = computed(() => (runData.value ? 'success' : 'ai'));
const isHovered = ref(false);
@ -55,7 +55,7 @@ function onClickAdd() {
v-if="isHandlePlusAvailable"
v-show="isHandlePlusVisible"
:handle-classes="handleClasses"
:status="plusStatus"
:type="plusType"
position="bottom"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"

View file

@ -42,7 +42,7 @@ describe('CanvasHandlePlus', () => {
it('should apply correct classes based on status', () => {
const { container } = renderComponent({
props: { status: 'success' },
props: { type: 'success' },
});
expect(container.firstChild).toHaveClass('success');

View file

@ -7,14 +7,14 @@ const props = withDefaults(
handleClasses?: string;
plusSize?: number;
lineSize?: number;
status?: 'success' | 'ai' | 'default';
type?: 'success' | 'ai' | 'default';
}>(),
{
position: 'right',
handleClasses: undefined,
plusSize: 24,
lineSize: 46,
status: 'default',
type: 'default',
},
);
@ -27,7 +27,7 @@ const style = useCssModule();
const classes = computed(() => [
style.wrapper,
style[props.position],
style[props.status],
style[props.type],
props.handleClasses,
]);

View file

@ -81,10 +81,11 @@ describe('useCanvasOperations', () => {
},
};
beforeEach(async () => {
beforeEach(() => {
vi.clearAllMocks();
const pinia = createTestingPinia({ initialState });
setActivePinia(pinia);
vi.clearAllMocks();
});
describe('requireNodeTypeDescription', () => {
@ -323,10 +324,12 @@ describe('useCanvasOperations', () => {
createTestWorkflowObject(workflowsStore.workflow),
);
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValue([
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
{ type: NodeConnectionType.AiTool },
]);
vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([NodeConnectionType.AiTool]);
vi.spyOn(NodeHelpers, 'getConnectionTypes')
.mockReturnValueOnce([NodeConnectionType.AiTool])
.mockReturnValueOnce([NodeConnectionType.AiTool]);
const { resolveNodePosition } = useCanvasOperations({ router });
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
@ -1075,29 +1078,53 @@ describe('useCanvasOperations', () => {
it('should return false if source and target nodes are the same', () => {
const node = mockNode({ id: '1', type: 'testType', name: 'Test Node' });
const { isConnectionAllowed } = useCanvasOperations({ router });
expect(isConnectionAllowed(node, node, NodeConnectionType.Main)).toBe(false);
expect(
isConnectionAllowed(node, node, NodeConnectionType.Main, NodeConnectionType.Main),
).toBe(false);
});
it('should return false if target node type does not have inputs', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
const sourceNode = mockNode({
id: '1',
type: 'sourceType',
name: 'Source Node',
});
const sourceNodeTypeDescription = mockNodeTypeDescription({
name: sourceNode.type,
outputs: [],
});
const targetNode = mockNode({
id: '2',
type: 'targetType',
name: 'Target Node',
});
const nodeTypeDescription = mockNodeTypeDescription({
name: 'targetType',
const targetNodeTypeDescription = mockNodeTypeDescription({
name: targetNode.type,
inputs: [],
});
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
nodeTypesStore.getNodeType = vi.fn(
(nodeTypeName: string) =>
({
[sourceNode.type]: sourceNodeTypeDescription,
[targetNode.type]: targetNodeTypeDescription,
})[nodeTypeName],
);
const { isConnectionAllowed } = useCanvasOperations({ router });
expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false);
expect(
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).toBe(false);
});
it('should return false if target node does not exist in the workflow', () => {
@ -1109,25 +1136,42 @@ describe('useCanvasOperations', () => {
type: 'sourceType',
name: 'Source Node',
});
const sourceNodeTypeDescription = mockNodeTypeDescription({
name: sourceNode.type,
outputs: [],
});
const targetNode = mockNode({
id: '2',
type: 'targetType',
name: 'Target Node',
});
const nodeTypeDescription = mockNodeTypeDescription({
name: 'targetType',
const targetNodeTypeDescription = mockNodeTypeDescription({
name: targetNode.type,
inputs: [NodeConnectionType.Main],
});
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
nodeTypesStore.getNodeType = vi.fn(
(nodeTypeName: string) =>
({
[sourceNode.type]: sourceNodeTypeDescription,
[targetNode.type]: targetNodeTypeDescription,
})[nodeTypeName],
);
const { isConnectionAllowed } = useCanvasOperations({ router });
expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false);
expect(
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).toBe(false);
});
it('should return false if input type does not match connection type', () => {
it('should return false if source node does not have connection type', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
const sourceNode = mockNode({
@ -1135,14 +1179,17 @@ describe('useCanvasOperations', () => {
type: 'sourceType',
name: 'Source Node',
});
const sourceNodeTypeDescription = mockNodeTypeDescription({
name: sourceNode.type,
outputs: [NodeConnectionType.Main],
});
const targetNode = mockNode({
id: '2',
type: 'targetType',
name: 'Target Node',
});
const nodeTypeDescription = mockNodeTypeDescription({
const targetNodeTypeDescription = mockNodeTypeDescription({
name: 'targetType',
inputs: [NodeConnectionType.AiTool],
});
@ -1154,9 +1201,70 @@ describe('useCanvasOperations', () => {
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
nodeTypesStore.getNodeType = vi.fn(
(nodeTypeName: string) =>
({
[sourceNode.type]: sourceNodeTypeDescription,
[targetNode.type]: targetNodeTypeDescription,
})[nodeTypeName],
);
expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false);
expect(
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.AiTool,
NodeConnectionType.AiTool,
),
).toBe(false);
});
it('should return false if target node does not have connection type', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
const sourceNode = mockNode({
id: '1',
type: 'sourceType',
name: 'Source Node',
});
const sourceNodeTypeDescription = mockNodeTypeDescription({
name: sourceNode.type,
outputs: [NodeConnectionType.Main],
});
const targetNode = mockNode({
id: '2',
type: 'targetType',
name: 'Target Node',
});
const targetNodeTypeDescription = mockNodeTypeDescription({
name: 'targetType',
inputs: [NodeConnectionType.AiTool],
});
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,
NodeConnectionType.Main,
NodeConnectionType.AiTool,
),
).toBe(false);
});
it('should return false if source node type is not allowed by target node input filter', () => {
@ -1168,6 +1276,10 @@ describe('useCanvasOperations', () => {
name: 'Source Node',
typeVersion: 1,
});
const sourceNodeTypeDescription = mockNodeTypeDescription({
name: sourceNode.type,
outputs: [NodeConnectionType.Main],
});
const targetNode = mockNode({
id: '2',
@ -1175,8 +1287,7 @@ describe('useCanvasOperations', () => {
name: 'Target Node',
typeVersion: 1,
});
const nodeTypeDescription = mockNodeTypeDescription({
const targetNodeTypeDescription = mockNodeTypeDescription({
name: 'targetType',
inputs: [
{
@ -1195,52 +1306,81 @@ describe('useCanvasOperations', () => {
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
nodeTypesStore.getNodeType = vi.fn(
(nodeTypeName: string) =>
({
[sourceNode.type]: sourceNodeTypeDescription,
[targetNode.type]: targetNodeTypeDescription,
})[nodeTypeName],
);
expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false);
expect(
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).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 targetNode = mockNode({
id: '2',
type: 'targetType',
name: 'Target Node',
typeVersion: 1,
});
const nodeTypeDescription = mockNodeTypeDescription({
name: 'targetType',
inputs: [
{
type: NodeConnectionType.Main,
filter: {
nodes: ['sourceType'],
},
},
],
});
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().mockReturnValue(nodeTypeDescription);
expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(true);
});
// 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 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 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,
// NodeConnectionType.Main,
// NodeConnectionType.Main,
// ),
// ).toBe(true);
// });
it('should return true if all conditions are met and no filter is set', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
@ -1252,6 +1392,10 @@ describe('useCanvasOperations', () => {
name: 'Source Node',
typeVersion: 1,
});
const sourceNodeTypeDescription = mockNodeTypeDescription({
name: sourceNode.type,
outputs: [NodeConnectionType.Main],
});
const targetNode = mockNode({
id: '2',
@ -1259,9 +1403,8 @@ describe('useCanvasOperations', () => {
name: 'Target Node',
typeVersion: 1,
});
const nodeTypeDescription = mockNodeTypeDescription({
name: 'targetType',
const targetNodeTypeDescription = mockNodeTypeDescription({
name: targetNode.type,
inputs: [
{
type: NodeConnectionType.Main,
@ -1276,9 +1419,22 @@ describe('useCanvasOperations', () => {
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
nodeTypesStore.getNodeType = vi.fn(
(nodeTypeName: string) =>
({
[sourceNode.type]: sourceNodeTypeDescription,
[targetNode.type]: targetNodeTypeDescription,
})[nodeTypeName],
);
expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(true);
expect(
isConnectionAllowed(
sourceNode,
targetNode,
NodeConnectionType.Main,
NodeConnectionType.Main,
),
).toBe(true);
});
});

View file

@ -1141,7 +1141,14 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
connection,
);
if (!isConnectionAllowed(sourceNode, targetNode, mappedConnection[1].type)) {
if (
!isConnectionAllowed(
sourceNode,
targetNode,
mappedConnection[0].type,
mappedConnection[1].type,
)
) {
return;
}
@ -1278,7 +1285,8 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
function isConnectionAllowed(
sourceNode: INodeUi,
targetNode: INodeUi,
connectionType: NodeConnectionType,
sourceConnectionType: NodeConnectionType,
targetConnectionType: NodeConnectionType,
): boolean {
const blocklist = [STICKY_NODE_TYPE];
@ -1286,52 +1294,77 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
return false;
}
if (sourceConnectionType !== targetConnectionType) {
return false;
}
if (blocklist.includes(sourceNode.type) || blocklist.includes(targetNode.type)) {
return false;
}
const sourceNodeType = nodeTypesStore.getNodeType(sourceNode.type, sourceNode.typeVersion);
const sourceWorkflowNode = editableWorkflowObject.value.getNode(sourceNode.name);
if (!sourceWorkflowNode) {
return false;
}
let sourceNodeOutputs: Array<NodeConnectionType | INodeOutputConfiguration> = [];
if (sourceNodeType) {
sourceNodeOutputs =
NodeHelpers.getNodeOutputs(
editableWorkflowObject.value,
sourceWorkflowNode,
sourceNodeType,
) || [];
}
const sourceNodeHasOutputConnectionOfType = !!sourceNodeOutputs.find((output) => {
const outputType = typeof output === 'string' ? output : output.type;
return outputType === sourceConnectionType;
});
if (!sourceNodeHasOutputConnectionOfType) {
return false;
}
const targetNodeType = nodeTypesStore.getNodeType(targetNode.type, targetNode.typeVersion);
if (targetNodeType?.inputs?.length) {
const workflowNode = editableWorkflowObject.value.getNode(targetNode.name);
if (!workflowNode) {
const targetWorkflowNode = editableWorkflowObject.value.getNode(targetNode.name);
if (!targetWorkflowNode) {
return false;
}
let targetNodeInputs: Array<NodeConnectionType | INodeInputConfiguration> = [];
if (targetNodeType) {
targetNodeInputs =
NodeHelpers.getNodeInputs(
editableWorkflowObject.value,
targetWorkflowNode,
targetNodeType,
) || [];
}
const targetNodeHasInputConnectionOfType = !!targetNodeInputs.find((input) => {
const inputType = typeof input === 'string' ? input : input.type;
if (inputType !== targetConnectionType) return false;
const filter = typeof input === 'object' && 'filter' in input ? input.filter : undefined;
if (filter?.nodes.length && !filter.nodes.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 false;
}
let inputs: Array<NodeConnectionType | INodeInputConfiguration> = [];
if (targetNodeType) {
inputs =
NodeHelpers.getNodeInputs(editableWorkflowObject.value, workflowNode, targetNodeType) ||
[];
}
return true;
});
let targetHasConnectionTypeAsInput = false;
for (const input of inputs) {
const inputType = typeof input === 'string' ? input : input.type;
if (inputType === connectionType) {
if (typeof input === 'object' && 'filter' in input && input.filter?.nodes.length) {
if (!input.filter.nodes.includes(sourceNode.type)) {
// this.dropPrevented = true;
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 false;
}
}
targetHasConnectionTypeAsInput = true;
}
}
return targetHasConnectionTypeAsInput;
}
return false;
return targetNodeHasInputConnectionOfType;
}
function addConnections(connections: CanvasConnectionCreateData[] | CanvasConnection[]) {