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:
Abhimanyu Yadav
2025-11-19 10:21:11 +05:30
committed by GitHub
parent 73c93cf554
commit 1154f86a5c
6 changed files with 96 additions and 24 deletions

View File

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

View File

@@ -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 && (

View File

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

View File

@@ -38,7 +38,7 @@ export const convertNodesPlusBlockInfoIntoCustomNodes = (
);
const customNode: CustomNode = {
id: node.id ?? "",
data: customNodeData,
data: { ...customNodeData, metadata: node.metadata },
type: "custom",
position: {
x:

View File

@@ -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",
});
},
},
});

View File

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