From 64c561ea63accc4412dfd0d9f9bc1ae6c22cc0d7 Mon Sep 17 00:00:00 2001 From: betterclever Date: Tue, 23 Dec 2025 19:07:21 +0530 Subject: [PATCH] feat: Allow renaming workflow nodes with inline editing - Double-click on node label to edit (Design mode only, not Entry Point) - Pencil icon in ConfigPanel header to rename (hover to reveal) - Shows component name as subscript when a custom label is set - Press Enter to save, Escape to cancel, or click outside to save - Compact UI: merged into component info section Signed-off-by: betterclever --- .../src/components/workflow/ConfigPanel.tsx | 73 ++++++++++++++++++- .../src/components/workflow/WorkflowNode.tsx | 73 ++++++++++++++++++- 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/workflow/ConfigPanel.tsx b/frontend/src/components/workflow/ConfigPanel.tsx index e5e4f42..007c955 100644 --- a/frontend/src/components/workflow/ConfigPanel.tsx +++ b/frontend/src/components/workflow/ConfigPanel.tsx @@ -1,6 +1,6 @@ import * as LucideIcons from 'lucide-react' import { useEffect, useState, useRef, useCallback } from 'react' -import { X, ExternalLink, Loader2, Trash2, ChevronDown, ChevronRight, Circle, CheckCircle2, AlertCircle } from 'lucide-react' +import { X, ExternalLink, Loader2, Trash2, ChevronDown, ChevronRight, Circle, CheckCircle2, AlertCircle, Pencil, Check } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -452,6 +452,18 @@ export function ConfigPanel({ const [dynamicInputs, setDynamicInputs] = useState(null) const [dynamicOutputs, setDynamicOutputs] = useState(null) + // Node name editing state + const [isEditingNodeName, setIsEditingNodeName] = useState(false) + const [editingNodeName, setEditingNodeName] = useState('') + + const handleSaveNodeName = useCallback(() => { + const trimmedName = editingNodeName.trim() + if (trimmedName && trimmedName !== nodeData.label) { + onUpdateNode?.(selectedNode.id, { label: trimmedName }) + } + setIsEditingNodeName(false) + }, [editingNodeName, nodeData.label, onUpdateNode, selectedNode.id]) + // Debounce ref const assertPortResolution = useRef(null) @@ -597,7 +609,7 @@ export function ConfigPanel({ - {/* Component Info */} + {/* Component Info with inline Node Name editing */}
@@ -619,7 +631,62 @@ export function ConfigPanel({ )} />
-

{component.name}

+ {/* Node Name - editable for non-entry-point nodes */} + {!isEntryPointComponent && isEditingNodeName ? ( +
+ setEditingNodeName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleSaveNodeName() + } else if (e.key === 'Escape') { + setIsEditingNodeName(false) + } + }} + onBlur={handleSaveNodeName} + placeholder={component.name} + className="h-6 text-sm font-medium py-0 px-1" + autoFocus + /> + +
+ ) : ( +
+

+ {nodeData.label || component.name} +

+ {!isEntryPointComponent && ( + + )} +
+ )} + {/* Show component name as subscript if custom name is set */} + {nodeData.label && nodeData.label !== component.name && ( + + {component.name} + + )}

{component.description}

diff --git a/frontend/src/components/workflow/WorkflowNode.tsx b/frontend/src/components/workflow/WorkflowNode.tsx index d734c7d..3169b9d 100644 --- a/frontend/src/components/workflow/WorkflowNode.tsx +++ b/frontend/src/components/workflow/WorkflowNode.tsx @@ -327,6 +327,11 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { const [isTerminalLoading, setIsTerminalLoading] = useState(false) const nodeRef = useRef(null) + // Inline label editing state + const [isEditingLabel, setIsEditingLabel] = useState(false) + const [editingLabelValue, setEditingLabelValue] = useState('') + const labelInputRef = useRef(null) + // Entry Point specific state const navigate = useNavigate() const [showWebhookDialog, setShowWebhookDialog] = useState(false) @@ -573,6 +578,45 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { // Display label (custom or component name) const displayLabel = data.label || component.name + // Check if user has set a custom label (different from component name) + const hasCustomLabel = data.label && data.label !== component.name + + // Label editing handlers + const handleStartEditing = () => { + if (isEntryPoint || mode !== 'design') return + setEditingLabelValue(data.label || component.name) + setIsEditingLabel(true) + // Focus the input after render + setTimeout(() => labelInputRef.current?.focus(), 0) + } + + const handleSaveLabel = () => { + const trimmedValue = editingLabelValue.trim() + if (trimmedValue && trimmedValue !== data.label) { + setNodes((nodes) => + nodes.map((n) => + n.id === id + ? { ...n, data: { ...n.data, label: trimmedValue } } + : n + ) + ) + markDirty() + } + setIsEditingLabel(false) + } + + const handleCancelEditing = () => { + setIsEditingLabel(false) + } + + const handleLabelKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + handleSaveLabel() + } else if (e.key === 'Escape') { + handleCancelEditing() + } + } // Check if there are unfilled required parameters or inputs const componentParameters = component.parameters ?? [] @@ -851,7 +895,34 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => {
-

{displayLabel}

+ {isEditingLabel ? ( + setEditingLabelValue(e.target.value)} + onBlur={handleSaveLabel} + onKeyDown={handleLabelKeyDown} + className="text-sm font-semibold bg-transparent border-b border-primary outline-none w-full py-0" + autoFocus + /> + ) : ( +
+

{displayLabel}

+ {hasCustomLabel && ( + + {component.name} + + )} +
+ )}
{/* Delete button (Design Mode only, not Entry Point) */}