Skip to content

Commit

Permalink
feat(frontend): Allow copy and pasting of blocks between flows (#8346)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bentlybro authored Oct 16, 2024
1 parent 8502928 commit 3982e20
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 61 deletions.
66 changes: 5 additions & 61 deletions autogpt_platform/frontend/src/components/Flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(() => {
Expand Down
122 changes: 122 additions & 0 deletions autogpt_platform/frontend/src/hooks/useCopyPaste.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 3982e20

Please sign in to comment.