fix(editor): Remove invalid connections after node handles change (#12247)

This commit is contained in:
Alex Grozav 2024-12-17 16:57:40 +02:00 committed by GitHub
parent 4d8e9cfc61
commit 6330bec4db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 433 additions and 32 deletions

View file

@ -39,6 +39,8 @@ const emit = defineEmits<{
'update:node:selected': [id: string];
'update:node:name': [id: string];
'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];
'run: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);
}
function onUpdateNodeInputs(id: string) {
emit('update:node:inputs', id);
}
function onUpdateNodeOutputs(id: string) {
emit('update:node:outputs', id);
}
/**
* Connections / Edges
*/
@ -679,6 +689,8 @@ provide(CanvasKey, {
@activate="onSetNodeActive"
@open:contextmenu="onOpenNodeContextMenu"
@update="onUpdateNodeParameters"
@update:inputs="onUpdateNodeInputs"
@update:outputs="onUpdateNodeOutputs"
@move="onUpdateNodePosition"
@add="onClickNodeAdd"
/>

View file

@ -31,6 +31,7 @@ import { useCanvas } from '@/composables/useCanvas';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
import type { EventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system';
import { isEqual } from 'lodash-es';
type Props = NodeProps<CanvasNodeData> & {
readOnly?: boolean;
@ -47,6 +48,8 @@ const emit = defineEmits<{
activate: [id: string];
'open:contextmenu': [id: string, event: MouseEvent, source: 'node-button' | 'node-right-click'];
update: [id: string, parameters: Record<string, unknown>];
'update:inputs': [id: string];
'update:outputs': [id: string];
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(() => {
props.eventBus?.on('nodes:action', emitCanvasNodeEvent);
});

View file

@ -1161,13 +1161,14 @@ describe('useCanvasMapping', () => {
expect(mappedConnections.value).toEqual([
{
data: {
fromNodeName: manualTriggerNode.name,
source: {
node: manualTriggerNode.name,
index: 0,
type: NodeConnectionType.Main,
},
status: undefined,
target: {
node: setNode.name,
index: 0,
type: NodeConnectionType.Main,
},
@ -1249,13 +1250,14 @@ describe('useCanvasMapping', () => {
expect(mappedConnections.value).toEqual([
{
data: {
fromNodeName: manualTriggerNode.name,
source: {
node: manualTriggerNode.name,
index: 0,
type: NodeConnectionType.AiTool,
},
status: undefined,
target: {
node: setNode.name,
index: 0,
type: NodeConnectionType.AiTool,
},
@ -1271,13 +1273,14 @@ describe('useCanvasMapping', () => {
},
{
data: {
fromNodeName: manualTriggerNode.name,
source: {
node: manualTriggerNode.name,
index: 0,
type: NodeConnectionType.AiDocument,
},
status: undefined,
target: {
node: setNode.name,
index: 1,
type: NodeConnectionType.AiDocument,
},

View file

@ -614,7 +614,7 @@ export function useCanvasMapping({
}
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) {
return '';
}

View file

@ -32,6 +32,7 @@ import { waitFor } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';
import { mockedStore } from '@/__tests__/utils';
import {
AGENT_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
SET_NODE_TYPE,
STICKY_NODE_TYPE,
@ -41,6 +42,7 @@ import {
import type { Connection } from '@vue-flow/core';
import { useClipboard } from '@/composables/useClipboard';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
import { nextTick } from 'vue';
vi.mock('vue-router', async (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', () => {
it('should delete all connections for a given node ID', () => {
const workflowsStore = mockedStore(useWorkflowsStore);

View file

@ -52,6 +52,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import type {
CanvasConnection,
CanvasConnectionCreateData,
CanvasConnectionPort,
CanvasNode,
CanvasNodeMoveEvent,
} 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(
sourceNode: INodeUi,
targetNode: INodeUi,
sourceConnection: IConnection,
targetConnection: IConnection,
sourceConnection: IConnection | CanvasConnectionPort,
targetConnection: IConnection | CanvasConnectionPort,
): boolean {
const blocklist = [STICKY_NODE_TYPE];
@ -1908,6 +1961,8 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
deleteConnection,
revertDeleteConnection,
deleteConnectionsByNodeId,
revalidateNodeInputConnections,
revalidateNodeOutputConnections,
isConnectionAllowed,
filterConnectionsByNodes,
importWorkflowData,

View file

@ -1,10 +1,5 @@
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
import type {
ExecutionStatus,
INodeConnections,
IConnection,
NodeConnectionType,
} from 'n8n-workflow';
import type { ExecutionStatus, INodeConnections, NodeConnectionType } from 'n8n-workflow';
import type {
DefaultEdge,
Node,
@ -15,11 +10,8 @@ import type {
} from '@vue-flow/core';
import type { IExecutionResponse, INodeUi } from '@/Interface';
import type { ComputedRef, Ref } from 'vue';
import type { PartialBy } from '@/utils/typeHelpers';
import type { EventBus } from 'n8n-design-system';
export type CanvasConnectionPortType = NodeConnectionType;
export const enum CanvasConnectionMode {
Input = 'inputs',
Output = 'outputs',
@ -31,10 +23,11 @@ export const canvasConnectionModes = [
] as const;
export type CanvasConnectionPort = {
type: CanvasConnectionPortType;
node?: string;
type: NodeConnectionType;
index: number;
required?: boolean;
maxConnections?: number;
index: number;
label?: string;
};
@ -124,7 +117,6 @@ export type CanvasNode = Node<CanvasNodeData>;
export interface CanvasConnectionData {
source: CanvasConnectionPort;
target: CanvasConnectionPort;
fromNodeName?: string;
status?: 'success' | 'error' | 'pinned' | 'running';
maxConnections?: number;
}
@ -137,8 +129,8 @@ export type CanvasConnectionCreateData = {
target: string;
targetHandle: string;
data: {
source: PartialBy<IConnection, 'node'>;
target: PartialBy<IConnection, 'node'>;
source: CanvasConnectionPort;
target: CanvasConnectionPort;
};
};

View file

@ -77,12 +77,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle,
targetHandle,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 0,
type: NodeConnectionType.Main,
},
target: {
node: nodes[1].name,
index: 0,
type: NodeConnectionType.Main,
},
@ -215,12 +216,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle: sourceHandleA,
targetHandle: targetHandleA,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 0,
type: NodeConnectionType.Main,
},
target: {
node: nodes[1].name,
index: 0,
type: NodeConnectionType.Main,
},
@ -233,12 +235,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle: sourceHandleB,
targetHandle: targetHandleB,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 1,
type: NodeConnectionType.Main,
},
target: {
node: nodes[1].name,
index: 1,
type: NodeConnectionType.Main,
},
@ -334,12 +337,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle: sourceHandleA,
targetHandle: targetHandleA,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 0,
type: NodeConnectionType.Main,
},
target: {
node: nodes[1].name,
index: 0,
type: NodeConnectionType.Main,
},
@ -352,12 +356,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle: sourceHandleB,
targetHandle: targetHandleB,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 1,
type: NodeConnectionType.Main,
},
target: {
node: nodes[2].name,
index: 0,
type: NodeConnectionType.Main,
},
@ -475,12 +480,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle: sourceHandleA,
targetHandle: targetHandleA,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 0,
type: NodeConnectionType.Main,
},
target: {
node: nodes[1].name,
index: 0,
type: NodeConnectionType.Main,
},
@ -493,12 +499,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle: sourceHandleB,
targetHandle: targetHandleB,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 0,
type: NodeConnectionType.AiMemory,
},
target: {
node: nodes[2].name,
index: 1,
type: NodeConnectionType.AiMemory,
},
@ -511,12 +518,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle: sourceHandleC,
targetHandle: targetHandleC,
data: {
fromNodeName: nodes[1].name,
source: {
node: nodes[1].name,
index: 0,
type: NodeConnectionType.Main,
},
target: {
node: nodes[2].name,
index: 0,
type: NodeConnectionType.Main,
},
@ -587,12 +595,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle,
targetHandle,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 1,
type: NodeConnectionType.Main,
},
target: {
node: nodes[1].name,
index: 0,
type: NodeConnectionType.Main,
},
@ -662,12 +671,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
sourceHandle,
targetHandle,
data: {
fromNodeName: nodes[0].name,
source: {
node: nodes[0].name,
index: 1,
type: NodeConnectionType.Main,
},
target: {
node: nodes[1].name,
index: 0,
type: NodeConnectionType.Main,
},

View file

@ -22,7 +22,8 @@ export function mapLegacyConnectionsToCanvasConnections(
const fromPorts = legacyConnections[fromNodeName][fromConnectionType];
fromPorts?.forEach((toPorts, fromIndex) => {
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 toIndex = toPort.index;
@ -53,12 +54,13 @@ export function mapLegacyConnectionsToCanvasConnections(
sourceHandle,
targetHandle,
data: {
fromNodeName,
source: {
node: fromNodeName,
index: fromIndex,
type: fromConnectionType,
},
target: {
node: toNodeName,
index: toIndex,
type: toConnectionType,
},

View file

@ -180,6 +180,8 @@ const {
revertCreateConnection,
deleteConnection,
revertDeleteConnection,
revalidateNodeInputConnections,
revalidateNodeOutputConnections,
setNodeActiveByName,
addConnections,
importWorkflowData,
@ -723,6 +725,14 @@ function onUpdateNodeParameters(id: string, parameters: Record<string, unknown>)
setNodeParameters(id, parameters);
}
function onUpdateNodeInputs(id: string) {
revalidateNodeInputConnections(id);
}
function onUpdateNodeOutputs(id: string) {
revalidateNodeOutputConnections(id);
}
function onClickNodeAdd(source: string, sourceHandle: string) {
nodeCreatorStore.openNodeCreatorForConnectingNode({
connection: {
@ -1618,6 +1628,8 @@ onBeforeUnmount(() => {
@update:node:enabled="onToggleNodeDisabled"
@update:node:name="onOpenRenameNodeModal"
@update:node:parameters="onUpdateNodeParameters"
@update:node:inputs="onUpdateNodeInputs"
@update:node:outputs="onUpdateNodeOutputs"
@click:node:add="onClickNodeAdd"
@run:node="onRunWorkflowToNode"
@delete:node="onDeleteNode"