fix(frontend/builder): Fix moved blocks disappearing on save (#10951)

- Resolves #10926
- Fixes a bug introduced in #10779

### Changes 🏗️

- Fix `.metadata.position` in graph save payload
- Make node reconciliation after graph save more robust

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Moved nodes don't disappear on graph save
This commit is contained in:
Reinier van der Leer
2025-09-18 15:34:06 +02:00
committed by GitHub
parent a978e91271
commit 3f19cba28f

View File

@@ -161,60 +161,14 @@ export default function useAgentGraph(
setAgentDescription(graph.description);
setAgentRecommendedScheduleCron(graph.recommended_schedule_cron || "");
const getGraphName = (node: Node) => {
if (node.input_default.agent_name) {
return node.input_default.agent_name;
}
return (
availableFlows.find((flow) => flow.id === node.input_default.graph_id)
?.name || null
);
};
setXYNodes((prevNodes) => {
const _newNodes = graph.nodes.map((node) => {
const block = availableBlocks.find(
(block) => block.id === node.block_id,
)!;
if (!block) return null;
const prevNode = prevNodes.find((n) => n.id === node.id);
const graphName =
(block.uiType == BlockUIType.AGENT && getGraphName(node)) || null;
const newNode: CustomNode = {
id: node.id,
type: "custom",
position: {
x: node?.metadata?.position?.x || 0,
y: node?.metadata?.position?.y || 0,
},
data: {
isOutputOpen: false,
...prevNode?.data,
block_id: block.id,
blockType: graphName || block.name,
blockCosts: block.costs,
categories: block.categories,
description: block.description,
title: `${block.name} ${node.id}`,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
hardcodedValues: node.input_default,
uiType: block.uiType,
metadata: node.metadata,
connections: graph.links
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
.map((link) => ({
edge_id: formatEdgeID(link),
source: link.source_id,
sourceHandle: link.source_name,
target: link.sink_id,
targetHandle: link.sink_name,
})),
backend_id: node.id,
},
};
return newNode;
});
const _newNodes = graph.nodes.map((node) =>
_backendNodeToXYNode(
node,
graph,
prevNodes.find((n) => n.id === node.id),
),
);
const newNodes = _newNodes.filter((n) => n !== null);
setXYEdges(() =>
graph.links.map((link) => {
@@ -250,6 +204,63 @@ export default function useAgentGraph(
});
}
function _backendNodeToXYNode(
node: Node,
graph: Graph,
prevNode?: CustomNode,
): CustomNode | null {
const block = availableBlocks.find((block) => block.id === node.block_id)!;
if (!block) return null;
const { position, ...metadata } = node.metadata;
const subGraphName =
(block.uiType == BlockUIType.AGENT && _getSubGraphName(node)) || null;
return {
id: node.id,
type: "custom",
position: {
x: position?.x || 0,
y: position?.y || 0,
},
data: {
isOutputOpen: false,
...prevNode?.data,
block_id: block.id,
blockType: subGraphName || block.name,
blockCosts: block.costs,
categories: block.categories,
description: block.description,
title: `${block.name} ${node.id}`,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
hardcodedValues: node.input_default,
uiType: block.uiType,
metadata: metadata,
connections: graph.links
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
.map((link) => ({
edge_id: formatEdgeID(link),
source: link.source_id,
sourceHandle: link.source_name,
target: link.sink_id,
targetHandle: link.sink_name,
})),
backend_id: node.id,
},
};
}
const _getSubGraphName = (node: Node) => {
if (node.input_default.agent_name) {
return node.input_default.agent_name;
}
return (
availableFlows.find((flow) => flow.id === node.input_default.graph_id)
?.name || null
);
};
const getFrontendId = useCallback(
(backendId: string, nodes: CustomNode[]) => {
const node = nodes.find((node) => node.data.backend_id === backendId);
@@ -622,8 +633,8 @@ export default function useAgentGraph(
block_id: node.data.block_id,
input_default: prepareNodeInputData(node),
metadata: {
...(node.data.metadata ?? {}),
position: node.position,
...(node.data.metadata || {}),
},
}),
),
@@ -641,7 +652,7 @@ export default function useAgentGraph(
const _saveAgent = useCallback(async () => {
// FIXME: frontend IDs should be resolved better (e.g. returned from the server)
// currently this relays on block_id and position
// currently this relies on block_id and position
const blockIDToNodeIDMap = Object.fromEntries(
xyNodes.map((node) => [
`${node.data.block_id}_${node.position.x}_${node.position.y}`,
@@ -691,10 +702,11 @@ export default function useAgentGraph(
const frontendNodeID = blockIDToNodeIDMap[key];
const frontendNode = prev.find((node) => node.id === frontendNodeID);
const { position, ...metadata } = backendNode.metadata;
return frontendNode
? {
? ({
...frontendNode,
position: backendNode.metadata.position,
position,
data: {
...frontendNode.data,
hardcodedValues: removeEmptyStringsAndNulls(
@@ -702,12 +714,11 @@ export default function useAgentGraph(
),
status: undefined,
backend_id: backendNode.id,
webhook: backendNode.webhook,
executionResults: [],
metadata: backendNode.metadata,
metadata,
},
}
: null;
} satisfies CustomNode)
: _backendNodeToXYNode(backendNode, newSavedAgent); // fallback
})
.filter((node) => node !== null);
});