From cd6a8cbd474abdffd4419bd36906e7f878722d3b Mon Sep 17 00:00:00 2001 From: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:11:25 +0530 Subject: [PATCH] fix(frontend): Include edges when copying multiple nodes with Shift+Select (#11478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes issue where edges were not being copied when selecting multiple nodes with Shift+Select. ### Changes 🏗️ - **Fixed edge copying logic**: Removed the requirement for edges to be explicitly selected - now automatically includes all edges between selected nodes when copying - **Migrated to custom stores**: Refactored copy-paste functionality to use `useNodeStore` and `useEdgeStore` instead of ReactFlow's built-in state management - **Improved type safety**: Replaced generic `Node` and `Edge` types with `CustomNode` and `CustomEdge` for better type checking - **Enhanced paste behavior**: - Deselects existing nodes before pasting to ensure only pasted nodes are selected - Uses store methods for adding nodes and edges, ensuring proper state management - **Simplified node data handling**: Removed manual clearing of backend_id, status, and nodeExecutionResult - now handled by the store's addNode method - **Added debugging support**: Added console logging for copied data to aid in troubleshooting ### 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] Select multiple nodes using Shift+Select and copy with Ctrl/Cmd+C - [x] Verify edges between selected nodes are included in the copy - [x] Paste with Ctrl/Cmd+V and confirm both nodes and edges appear - [x] Verify pasted elements maintain correct connections - [x] Confirm only pasted nodes are selected after paste - [x] Test that pasted nodes have unique IDs - [x] Verify pasted nodes appear centered in the current viewport --- .../FlowEditor/Flow/useCopyPaste.ts | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useCopyPaste.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useCopyPaste.ts index e78e04c628..1114fb778c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useCopyPaste.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useCopyPaste.ts @@ -1,16 +1,20 @@ import { useCallback } from "react"; -import { Node, Edge, useReactFlow } from "@xyflow/react"; +import { useReactFlow } from "@xyflow/react"; import { Key, storage } from "@/services/storage/local-storage"; import { v4 as uuidv4 } from "uuid"; +import { useNodeStore } from "../../../stores/nodeStore"; +import { useEdgeStore } from "../../../stores/edgeStore"; +import { CustomNode } from "../nodes/CustomNode/CustomNode"; +import { CustomEdge } from "../edges/CustomEdge"; interface CopyableData { - nodes: Node[]; - edges: Edge[]; + nodes: CustomNode[]; + edges: CustomEdge[]; } export function useCopyPaste() { - const { setNodes, addEdges, getNodes, getEdges, getViewport } = - useReactFlow(); + // Only use useReactFlow for viewport (not managed by stores) + const { getViewport } = useReactFlow(); const handleCopyPaste = useCallback( (event: KeyboardEvent) => { @@ -26,13 +30,15 @@ export function useCopyPaste() { if (event.ctrlKey || event.metaKey) { // COPY: Ctrl+C or Cmd+C if (event.key === "c" || event.key === "C") { - const selectedNodes = getNodes().filter((node) => node.selected); + const { nodes } = useNodeStore.getState(); + const { edges } = useEdgeStore.getState(); + + const selectedNodes = nodes.filter((node) => node.selected); const selectedNodeIds = new Set(selectedNodes.map((node) => node.id)); - // Only copy edges where both source and target nodes are selected - const selectedEdges = getEdges().filter( + // Copy edges where both source and target nodes are selected + const selectedEdges = edges.filter( (edge) => - edge.selected && selectedNodeIds.has(edge.source) && selectedNodeIds.has(edge.target), ); @@ -68,7 +74,7 @@ export function useCopyPaste() { minY = Infinity, maxX = -Infinity, maxY = -Infinity; - copiedData.nodes.forEach((node: Node) => { + copiedData.nodes.forEach((node) => { minX = Math.min(minX, node.position.x); minY = Math.min(minY, node.position.y); maxX = Math.max(maxX, node.position.x); @@ -78,50 +84,50 @@ export function useCopyPaste() { const offsetX = viewportCenter.x - (minX + maxX) / 2; const offsetY = viewportCenter.y - (minY + maxY) / 2; - // Create new nodes with UNIQUE IDs using UUID - const pastedNodes = copiedData.nodes.map((node: Node) => { - const newNodeId = uuidv4(); // Generate unique UUID for each node + // Deselect existing nodes first + useNodeStore.setState((state) => ({ + nodes: state.nodes.map((node) => ({ ...node, selected: false })), + })); + + // Create and add new nodes with UNIQUE IDs using UUID + copiedData.nodes.forEach((node) => { + const newNodeId = uuidv4(); oldToNewIdMap[node.id] = newNodeId; - return { + + const newNode: CustomNode = { ...node, - id: newNodeId, // Assign the new unique ID - selected: true, // Select the pasted nodes + id: newNodeId, + selected: true, position: { x: node.position.x + offsetX, y: node.position.y + offsetY, }, - data: { - ...node.data, - backend_id: undefined, // Clear backend_id so the new node.id is used when saving - status: undefined, // Clear execution status - nodeExecutionResult: undefined, // Clear execution results - }, }; + + useNodeStore.getState().addNode(newNode); }); - // Create new edges with updated source/target IDs - const pastedEdges = copiedData.edges.map((edge) => { + // Add edges with updated source/target IDs + const { addEdge } = useEdgeStore.getState(); + copiedData.edges.forEach((edge) => { const newSourceId = oldToNewIdMap[edge.source] ?? edge.source; const newTargetId = oldToNewIdMap[edge.target] ?? edge.target; - return { - ...edge, - id: `${newSourceId}_${edge.sourceHandle}_${newTargetId}_${edge.targetHandle}_${Date.now()}`, + + addEdge({ source: newSourceId, target: newTargetId, - }; + sourceHandle: edge.sourceHandle ?? "", + targetHandle: edge.targetHandle ?? "", + data: { + ...edge.data, + }, + }); }); - - // Deselect existing nodes and add pasted nodes - setNodes((existingNodes) => [ - ...existingNodes.map((node) => ({ ...node, selected: false })), - ...pastedNodes, - ]); - addEdges(pastedEdges); } } } }, - [setNodes, addEdges, getNodes, getEdges, getViewport], + [getViewport], ); return handleCopyPaste;