feat(frontend): add drag and drop functionality from blocks menu to flow canvas (#10777)

Closes #1055
This commit is contained in:
Swifty
2025-08-29 10:55:43 +02:00
committed by GitHub
parent 3e4ca19036
commit 15aa091f65
2 changed files with 101 additions and 2 deletions

View File

@@ -37,7 +37,11 @@ import {
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Key, storage } from "@/services/storage/local-storage";
import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils";
import {
getTypeColor,
findNewlyAddedBlockCoordinates,
beautifyString,
} from "@/lib/utils";
import { history } from "./history";
import { CustomEdge } from "./CustomEdge";
import ConnectionLine from "./ConnectionLine";
@@ -94,6 +98,7 @@ const FlowEditor: React.FC<{
updateNode,
getViewport,
setViewport,
screenToFlowPosition,
} = useReactFlow<CustomNode, CustomEdge>();
const [nodeId, setNodeId] = useState<number>(1);
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false);
@@ -678,6 +683,85 @@ const FlowEditor: React.FC<{
const isNewBlockEnabled = useGetFlag(Flag.NEW_BLOCK_MENU);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
}, []);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const blockData = event.dataTransfer.getData("application/reactflow");
if (!blockData) return;
try {
const { blockId, blockName, hardcodedValues } = JSON.parse(blockData);
// Convert screen coordinates to flow coordinates
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
// Find the block schema
const nodeSchema = availableBlocks.find((node) => node.id === blockId);
if (!nodeSchema) {
console.error(`Schema not found for block ID: ${blockId}`);
return;
}
// Create the new node at the drop position
const newNode: CustomNode = {
id: nodeId.toString(),
type: "custom",
position,
data: {
blockType: blockName,
blockCosts: nodeSchema.costs || [],
title: `${beautifyString(blockName)} ${nodeId}`,
description: nodeSchema.description,
categories: nodeSchema.categories,
inputSchema: nodeSchema.inputSchema,
outputSchema: nodeSchema.outputSchema,
hardcodedValues: hardcodedValues,
connections: [],
isOutputOpen: false,
block_id: blockId,
uiType: nodeSchema.uiType,
},
};
history.push({
type: "ADD_NODE",
payload: { node: { ...newNode, ...newNode.data } },
undo: () => {
deleteElements({ nodes: [{ id: newNode.id } as any], edges: [] });
},
redo: () => {
addNodes([newNode]);
},
});
addNodes([newNode]);
clearNodesStatusAndOutput();
setNodeId((prevId) => prevId + 1);
} catch (error) {
console.error("Failed to drop block:", error);
}
},
[
nodeId,
availableBlocks,
nodes,
edges,
addNodes,
screenToFlowPosition,
deleteElements,
clearNodesStatusAndOutput,
],
);
return (
<FlowContext.Provider
value={{ visualizeBeads, setIsAnyModalOpen, getNextNodeId }}
@@ -695,6 +779,8 @@ const FlowEditor: React.FC<{
onEdgesChange={onEdgesChange}
onNodeDragStop={onNodeDragEnd}
onNodeDragStart={onNodeDragStart}
onDrop={onDrop}
onDragOver={onDragOver}
deleteKeyCode={["Backspace", "Delete"]}
minZoom={0.1}
maxZoom={2}

View File

@@ -278,9 +278,22 @@ export function BlocksControl({
className={`m-2 my-4 flex h-20 shadow-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 ${
block.notAvailable
? "cursor-not-allowed opacity-50"
: "cursor-pointer hover:shadow-lg"
: "cursor-move hover:shadow-lg"
}`}
data-id={`block-card-${block.id}`}
draggable={!block.notAvailable}
onDragStart={(e) => {
if (block.notAvailable) return;
e.dataTransfer.effectAllowed = "copy";
e.dataTransfer.setData(
"application/reactflow",
JSON.stringify({
blockId: block.id,
blockName: block.name,
hardcodedValues: block?.hardcodedValues || {},
}),
);
}}
onClick={() =>
!block.notAvailable &&
addBlock(block.id, block.name, block?.hardcodedValues || {})