Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions frontend/src/components/workflow/ConfigPanel.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -452,6 +452,18 @@ export function ConfigPanel({
const [dynamicInputs, setDynamicInputs] = useState<any[] | null>(null)
const [dynamicOutputs, setDynamicOutputs] = useState<any[] | null>(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<NodeJS.Timeout | null>(null)

Expand Down Expand Up @@ -597,7 +609,7 @@ export function ConfigPanel({
</Button>
</div>

{/* Component Info */}
{/* Component Info with inline Node Name editing */}
<div className="px-4 py-3 border-b bg-muted/20">
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg border bg-background flex-shrink-0">
Expand All @@ -619,7 +631,62 @@ export function ConfigPanel({
)} />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm">{component.name}</h4>
{/* Node Name - editable for non-entry-point nodes */}
{!isEntryPointComponent && isEditingNodeName ? (
<div className="flex items-center gap-1">
<Input
type="text"
value={editingNodeName}
onChange={(e) => 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
/>
<Button
variant="ghost"
size="icon"
className="h-5 w-5 flex-shrink-0"
onClick={handleSaveNodeName}
>
<Check className="h-3 w-3" />
</Button>
</div>
) : (
<div className="flex items-center gap-1 group">
<h4 className="font-medium text-sm truncate">
{nodeData.label || component.name}
</h4>
{!isEntryPointComponent && (
<Button
variant="ghost"
size="icon"
className="h-5 w-5 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => {
setEditingNodeName(nodeData.label || component.name)
setIsEditingNodeName(true)
}}
title="Rename node"
>
<Pencil className="h-3 w-3" />
</Button>
)}
</div>
)}
{/* Show component name as subscript if custom name is set */}
{nodeData.label && nodeData.label !== component.name && (
<span className="text-[10px] text-muted-foreground opacity-70">
{component.name}
</span>
)}
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
{component.description}
</p>
Expand Down
73 changes: 72 additions & 1 deletion frontend/src/components/workflow/WorkflowNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {
const [isTerminalLoading, setIsTerminalLoading] = useState(false)
const nodeRef = useRef<HTMLDivElement | null>(null)

// Inline label editing state
const [isEditingLabel, setIsEditingLabel] = useState(false)
const [editingLabelValue, setEditingLabelValue] = useState('')
const labelInputRef = useRef<HTMLInputElement | null>(null)

// Entry Point specific state
const navigate = useNavigate()
const [showWebhookDialog, setShowWebhookDialog] = useState(false)
Expand Down Expand Up @@ -573,6 +578,45 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {

// 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 ?? []
Expand Down Expand Up @@ -851,7 +895,34 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<h3 className="text-sm font-semibold truncate">{displayLabel}</h3>
{isEditingLabel ? (
<input
ref={labelInputRef}
type="text"
value={editingLabelValue}
onChange={(e) => 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
/>
) : (
<div
className={cn(
"group/label",
!isEntryPoint && mode === 'design' && "cursor-text"
)}
onDoubleClick={handleStartEditing}
title={!isEntryPoint && mode === 'design' ? "Double-click to rename" : undefined}
>
<h3 className="text-sm font-semibold truncate">{displayLabel}</h3>
{hasCustomLabel && (
<span className="text-[10px] text-muted-foreground opacity-70 truncate block">
{component.name}
</span>
)}
</div>
)}
</div>
<div className="flex items-center gap-1">
{/* Delete button (Design Mode only, not Entry Point) */}
Expand Down