fix(frontend): Include edges when copying multiple nodes with Shift+Select (#11478)

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
This commit is contained in:
Abhimanyu Yadav
2025-12-01 11:11:25 +05:30
committed by GitHub
parent 7fabbb25c4
commit cd6a8cbd47

View File

@@ -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;