mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): Allow copy and pasting of blocks between flows (#8346)
This commit is contained in:
@@ -45,6 +45,7 @@ import RunnerUIWrapper, {
|
||||
import PrimaryActionBar from "@/components/PrimaryActionButton";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { forceLoad } from "@sentry/nextjs";
|
||||
import { useCopyPaste } from "../hooks/useCopyPaste";
|
||||
|
||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
@@ -459,6 +460,8 @@ const FlowEditor: React.FC<{
|
||||
history.redo();
|
||||
};
|
||||
|
||||
const handleCopyPaste = useCopyPaste(getNextNodeId);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// Prevent copy/paste if any modal is open or if the focus is on an input element
|
||||
@@ -470,68 +473,9 @@ const FlowEditor: React.FC<{
|
||||
|
||||
if (isAnyModalOpen || isInputField) return;
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (event.key === "c" || event.key === "C") {
|
||||
// Copy selected nodes
|
||||
const selectedNodes = nodes.filter((node) => node.selected);
|
||||
const selectedEdges = edges.filter((edge) => edge.selected);
|
||||
setCopiedNodes(selectedNodes);
|
||||
setCopiedEdges(selectedEdges);
|
||||
}
|
||||
if (event.key === "v" || event.key === "V") {
|
||||
// Paste copied nodes
|
||||
if (copiedNodes.length > 0) {
|
||||
const oldToNewNodeIDMap: Record<string, string> = {};
|
||||
const pastedNodes = copiedNodes.map((node, index) => {
|
||||
const newNodeId = (nodeId + index).toString();
|
||||
oldToNewNodeIDMap[node.id] = newNodeId;
|
||||
return {
|
||||
...node,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: node.position.x + 20, // Offset pasted nodes
|
||||
y: node.position.y + 20,
|
||||
},
|
||||
data: {
|
||||
...node.data,
|
||||
status: undefined, // Reset status
|
||||
executionResults: undefined, // Clear output data
|
||||
},
|
||||
};
|
||||
});
|
||||
setNodes((existingNodes) =>
|
||||
// Deselect copied nodes
|
||||
existingNodes.map((node) => ({ ...node, selected: false })),
|
||||
);
|
||||
addNodes(pastedNodes);
|
||||
setNodeId((prevId) => prevId + copiedNodes.length);
|
||||
|
||||
const pastedEdges = copiedEdges.map((edge) => {
|
||||
const newSourceId = oldToNewNodeIDMap[edge.source] ?? edge.source;
|
||||
const newTargetId = oldToNewNodeIDMap[edge.target] ?? edge.target;
|
||||
return {
|
||||
...edge,
|
||||
id: `${newSourceId}_${edge.sourceHandle}_${newTargetId}_${edge.targetHandle}_${Date.now()}`,
|
||||
source: newSourceId,
|
||||
target: newTargetId,
|
||||
};
|
||||
});
|
||||
addEdges(pastedEdges);
|
||||
}
|
||||
}
|
||||
}
|
||||
handleCopyPaste(event);
|
||||
},
|
||||
[
|
||||
isAnyModalOpen,
|
||||
nodes,
|
||||
edges,
|
||||
copiedNodes,
|
||||
setNodes,
|
||||
addNodes,
|
||||
copiedEdges,
|
||||
addEdges,
|
||||
nodeId,
|
||||
],
|
||||
[isAnyModalOpen, handleCopyPaste],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
122
autogpt_platform/frontend/src/hooks/useCopyPaste.ts
Normal file
122
autogpt_platform/frontend/src/hooks/useCopyPaste.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useCallback } from "react";
|
||||
import { Node, Edge, useReactFlow, useViewport } from "@xyflow/react";
|
||||
|
||||
export function useCopyPaste(getNextNodeId: () => string) {
|
||||
const { setNodes, addEdges, getNodes, getEdges } = useReactFlow();
|
||||
const { x, y, zoom } = useViewport();
|
||||
|
||||
const handleCopyPaste = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (event.key === "c" || event.key === "C") {
|
||||
const selectedNodes = getNodes().filter((node) => node.selected);
|
||||
const selectedEdges = getEdges().filter((edge) => edge.selected);
|
||||
|
||||
const copiedData = {
|
||||
nodes: selectedNodes.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: [],
|
||||
},
|
||||
})),
|
||||
edges: selectedEdges,
|
||||
};
|
||||
|
||||
localStorage.setItem("copiedFlowData", JSON.stringify(copiedData));
|
||||
}
|
||||
if (event.key === "v" || event.key === "V") {
|
||||
const copiedDataString = localStorage.getItem("copiedFlowData");
|
||||
if (copiedDataString) {
|
||||
const copiedData = JSON.parse(copiedDataString);
|
||||
const oldToNewIdMap: Record<string, string> = {};
|
||||
|
||||
const viewportCenter = {
|
||||
x: (window.innerWidth / 2 - x) / zoom,
|
||||
y: (window.innerHeight / 2 - y) / zoom,
|
||||
};
|
||||
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
copiedData.nodes.forEach((node: Node) => {
|
||||
minX = Math.min(minX, node.position.x);
|
||||
minY = Math.min(minY, node.position.y);
|
||||
maxX = Math.max(maxX, node.position.x);
|
||||
maxY = Math.max(maxY, node.position.y);
|
||||
});
|
||||
|
||||
const offsetX = viewportCenter.x - (minX + maxX) / 2;
|
||||
const offsetY = viewportCenter.y - (minY + maxY) / 2;
|
||||
|
||||
const pastedNodes = copiedData.nodes.map((node: Node) => {
|
||||
const newNodeId = getNextNodeId();
|
||||
oldToNewIdMap[node.id] = newNodeId;
|
||||
return {
|
||||
...node,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: node.position.x + offsetX,
|
||||
y: node.position.y + offsetY,
|
||||
},
|
||||
data: {
|
||||
...node.data,
|
||||
status: undefined,
|
||||
executionResults: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const pastedEdges = copiedData.edges.map((edge: 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()}`,
|
||||
source: newSourceId,
|
||||
target: newTargetId,
|
||||
};
|
||||
});
|
||||
|
||||
setNodes((existingNodes) => [
|
||||
...existingNodes.map((node) => ({ ...node, selected: false })),
|
||||
...pastedNodes,
|
||||
]);
|
||||
addEdges(pastedEdges);
|
||||
|
||||
setNodes((nodes) => {
|
||||
return nodes.map((node) => {
|
||||
if (oldToNewIdMap[node.id]) {
|
||||
const nodeConnections = pastedEdges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === node.id || edge.target === node.id,
|
||||
)
|
||||
.map((edge) => ({
|
||||
edge_id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
targetHandle: edge.targetHandle,
|
||||
}));
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: nodeConnections,
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[setNodes, addEdges, getNodes, getEdges, getNextNodeId, x, y, zoom],
|
||||
);
|
||||
|
||||
return handleCopyPaste;
|
||||
}
|
||||
Reference in New Issue
Block a user