mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(frontend): add inline node title editing with double-click (#11370)
- depends on https://github.com/Significant-Gravitas/AutoGPT/pull/11368 This PR adds the ability to rename nodes directly in the flow editor by double-clicking on their titles. https://github.com/user-attachments/assets/1de3fc5c-f859-425e-b4cf-dfb21c3efe3d ### Changes 🏗️ - **Added inline node title editing functionality:** - Users can now double-click on any node title to enter edit mode - Custom titles are saved on Enter key or blur, canceled on Escape key - Custom node names are persisted in the node's metadata as `customized_name` - Added tooltip to display full title when text is truncated - **Modified node data handling:** - Updated `nodeStore` to include `customized_name` in metadata when converting nodes - Modified `helper.ts` to pass metadata (including custom titles) to custom nodes - Added metadata property to `CustomNodeData` type - **UI improvements:** - Added hover cursor indication for editable titles - Implemented proper focus management during editing - Maintained consistent styling between display and edit modes ### 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] Double-click on various node types to enter edit mode - [x] Type new names and press Enter to save - [x] Press Escape to cancel editing and revert to original name - [x] Click outside the input field to save changes - [x] Verify custom names persist after page refresh - [x] Test with long node names to ensure tooltip appears - [x] Verify custom names are saved with the graph - [x] Test editing on all node types (standard, input, output, webhook, etc.)
This commit is contained in:
@@ -18,6 +18,7 @@ import { NodeExecutionBadge } from "./components/NodeExecutionBadge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||
import { AyrshareConnectButton } from "./components/AyrshareConnectButton";
|
||||
import { NodeModelMetadata } from "@/app/api/__generated__/models/nodeModelMetadata";
|
||||
|
||||
export type CustomNodeData = {
|
||||
hardcodedValues: {
|
||||
@@ -35,6 +36,7 @@ export type CustomNodeData = {
|
||||
// TODO : We need better type safety for the following backend fields.
|
||||
costs: BlockCost[];
|
||||
categories: BlockInfoCategoriesItem[];
|
||||
metadata?: NodeModelMetadata;
|
||||
};
|
||||
|
||||
export type CustomNode = XYNode<CustomNodeData, "custom">;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const NodeContextMenu = ({
|
||||
>
|
||||
<DropdownMenuItem onClick={handleCopy} className="hover:rounded-xlarge">
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy
|
||||
Copy Node
|
||||
</DropdownMenuItem>
|
||||
|
||||
{subGraphID && (
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { NodeCost } from "./NodeCost";
|
||||
import { NodeBadges } from "./NodeBadges";
|
||||
import { NodeContextMenu } from "./NodeContextMenu";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
|
||||
export const NodeHeader = ({
|
||||
data,
|
||||
@@ -12,31 +20,86 @@ export const NodeHeader = ({
|
||||
data: CustomNodeData;
|
||||
nodeId: string;
|
||||
}) => {
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
const title = (data.metadata?.customized_name as string) || data.title;
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [editedTitle, setEditedTitle] = useState(title);
|
||||
|
||||
const handleTitleEdit = () => {
|
||||
updateNodeData(nodeId, {
|
||||
metadata: { ...data.metadata, customized_name: editedTitle },
|
||||
});
|
||||
setIsEditingTitle(false);
|
||||
};
|
||||
|
||||
const handleTitleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") handleTitleEdit();
|
||||
if (e.key === "Escape") {
|
||||
setEditedTitle(title);
|
||||
setIsEditingTitle(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-auto items-start justify-between gap-2 rounded-xlarge border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Upper section */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
variant="large-semibold"
|
||||
className="tracking-tight text-slate-800"
|
||||
<div className="flex h-auto flex-col gap-1 rounded-xlarge border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4 pt-3">
|
||||
{/* Title row with context menu */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<div
|
||||
onDoubleClick={() => setIsEditingTitle(true)}
|
||||
className="flex w-fit min-w-0 flex-1 items-center hover:cursor-pointer"
|
||||
>
|
||||
{beautifyString(data.title)}
|
||||
</Text>
|
||||
<Text variant="small" className="!font-medium !text-slate-500">
|
||||
#{nodeId.split("-")[0]}
|
||||
</Text>
|
||||
</div>
|
||||
{/* Lower section */}
|
||||
<div className="flex space-x-2">
|
||||
<NodeCost blockCosts={data.costs} nodeId={nodeId} />
|
||||
<NodeBadges categories={data.categories} />
|
||||
{isEditingTitle ? (
|
||||
<input
|
||||
id="node-title-input"
|
||||
value={editedTitle}
|
||||
onChange={(e) => setEditedTitle(e.target.value)}
|
||||
autoFocus
|
||||
className={cn(
|
||||
"m-0 h-fit w-full border-none bg-transparent p-0 focus:outline-none focus:ring-0",
|
||||
"font-sans text-[1rem] font-semibold leading-[1.5rem] text-zinc-800",
|
||||
)}
|
||||
onBlur={handleTitleEdit}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Text variant="large-semibold" className="line-clamp-1">
|
||||
{beautifyString(title)}
|
||||
</Text>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{beautifyString(title)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
variant="small"
|
||||
className="shrink-0 !font-medium !text-slate-500"
|
||||
>
|
||||
#{nodeId.split("-")[0]}
|
||||
</Text>
|
||||
<NodeContextMenu
|
||||
subGraphID={data.hardcodedValues?.graph_id}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NodeContextMenu
|
||||
subGraphID={data.hardcodedValues?.graph_id}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
|
||||
{/* Metadata row */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<NodeCost blockCosts={data.costs} nodeId={nodeId} />
|
||||
<NodeBadges categories={data.categories} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const convertNodesPlusBlockInfoIntoCustomNodes = (
|
||||
);
|
||||
const customNode: CustomNode = {
|
||||
id: node.id ?? "",
|
||||
data: customNodeData,
|
||||
data: { ...customNodeData, metadata: node.metadata },
|
||||
type: "custom",
|
||||
position: {
|
||||
x:
|
||||
|
||||
@@ -69,6 +69,12 @@ export const useSaveGraph = ({
|
||||
},
|
||||
onError: (error) => {
|
||||
onError?.(error);
|
||||
toast({
|
||||
title: "Error saving graph",
|
||||
description:
|
||||
(error as any).message ?? "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -136,6 +136,7 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
metadata: {
|
||||
// TODO: Add more metadata
|
||||
position: node.position,
|
||||
customized_name: node.data.metadata?.customized_name,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user