Skip to content

Commit 86c7284

Browse files
authored
Merge pull request #187 from ShipSecAI/betterclever/25b2026/d2feat1
feat: Allow renaming workflow nodes with inline editing
2 parents ebc78de + 64c561e commit 86c7284

File tree

2 files changed

+142
-4
lines changed

2 files changed

+142
-4
lines changed

frontend/src/components/workflow/ConfigPanel.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as LucideIcons from 'lucide-react'
22
import { useEffect, useState, useRef, useCallback } from 'react'
3-
import { X, ExternalLink, Loader2, Trash2, ChevronDown, ChevronRight, Circle, CheckCircle2, AlertCircle } from 'lucide-react'
3+
import { X, ExternalLink, Loader2, Trash2, ChevronDown, ChevronRight, Circle, CheckCircle2, AlertCircle, Pencil, Check } from 'lucide-react'
44
import { useNavigate } from 'react-router-dom'
55
import { Button } from '@/components/ui/button'
66
import { Input } from '@/components/ui/input'
@@ -452,6 +452,18 @@ export function ConfigPanel({
452452
const [dynamicInputs, setDynamicInputs] = useState<any[] | null>(null)
453453
const [dynamicOutputs, setDynamicOutputs] = useState<any[] | null>(null)
454454

455+
// Node name editing state
456+
const [isEditingNodeName, setIsEditingNodeName] = useState(false)
457+
const [editingNodeName, setEditingNodeName] = useState('')
458+
459+
const handleSaveNodeName = useCallback(() => {
460+
const trimmedName = editingNodeName.trim()
461+
if (trimmedName && trimmedName !== nodeData.label) {
462+
onUpdateNode?.(selectedNode.id, { label: trimmedName })
463+
}
464+
setIsEditingNodeName(false)
465+
}, [editingNodeName, nodeData.label, onUpdateNode, selectedNode.id])
466+
455467
// Debounce ref
456468
const assertPortResolution = useRef<NodeJS.Timeout | null>(null)
457469

@@ -597,7 +609,7 @@ export function ConfigPanel({
597609
</Button>
598610
</div>
599611

600-
{/* Component Info */}
612+
{/* Component Info with inline Node Name editing */}
601613
<div className="px-4 py-3 border-b bg-muted/20">
602614
<div className="flex items-start gap-3">
603615
<div className="p-2 rounded-lg border bg-background flex-shrink-0">
@@ -619,7 +631,62 @@ export function ConfigPanel({
619631
)} />
620632
</div>
621633
<div className="flex-1 min-w-0">
622-
<h4 className="font-medium text-sm">{component.name}</h4>
634+
{/* Node Name - editable for non-entry-point nodes */}
635+
{!isEntryPointComponent && isEditingNodeName ? (
636+
<div className="flex items-center gap-1">
637+
<Input
638+
type="text"
639+
value={editingNodeName}
640+
onChange={(e) => setEditingNodeName(e.target.value)}
641+
onKeyDown={(e) => {
642+
if (e.key === 'Enter') {
643+
e.preventDefault()
644+
handleSaveNodeName()
645+
} else if (e.key === 'Escape') {
646+
setIsEditingNodeName(false)
647+
}
648+
}}
649+
onBlur={handleSaveNodeName}
650+
placeholder={component.name}
651+
className="h-6 text-sm font-medium py-0 px-1"
652+
autoFocus
653+
/>
654+
<Button
655+
variant="ghost"
656+
size="icon"
657+
className="h-5 w-5 flex-shrink-0"
658+
onClick={handleSaveNodeName}
659+
>
660+
<Check className="h-3 w-3" />
661+
</Button>
662+
</div>
663+
) : (
664+
<div className="flex items-center gap-1 group">
665+
<h4 className="font-medium text-sm truncate">
666+
{nodeData.label || component.name}
667+
</h4>
668+
{!isEntryPointComponent && (
669+
<Button
670+
variant="ghost"
671+
size="icon"
672+
className="h-5 w-5 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
673+
onClick={() => {
674+
setEditingNodeName(nodeData.label || component.name)
675+
setIsEditingNodeName(true)
676+
}}
677+
title="Rename node"
678+
>
679+
<Pencil className="h-3 w-3" />
680+
</Button>
681+
)}
682+
</div>
683+
)}
684+
{/* Show component name as subscript if custom name is set */}
685+
{nodeData.label && nodeData.label !== component.name && (
686+
<span className="text-[10px] text-muted-foreground opacity-70">
687+
{component.name}
688+
</span>
689+
)}
623690
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
624691
{component.description}
625692
</p>

frontend/src/components/workflow/WorkflowNode.tsx

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {
327327
const [isTerminalLoading, setIsTerminalLoading] = useState(false)
328328
const nodeRef = useRef<HTMLDivElement | null>(null)
329329

330+
// Inline label editing state
331+
const [isEditingLabel, setIsEditingLabel] = useState(false)
332+
const [editingLabelValue, setEditingLabelValue] = useState('')
333+
const labelInputRef = useRef<HTMLInputElement | null>(null)
334+
330335
// Entry Point specific state
331336
const navigate = useNavigate()
332337
const [showWebhookDialog, setShowWebhookDialog] = useState(false)
@@ -573,6 +578,45 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {
573578

574579
// Display label (custom or component name)
575580
const displayLabel = data.label || component.name
581+
// Check if user has set a custom label (different from component name)
582+
const hasCustomLabel = data.label && data.label !== component.name
583+
584+
// Label editing handlers
585+
const handleStartEditing = () => {
586+
if (isEntryPoint || mode !== 'design') return
587+
setEditingLabelValue(data.label || component.name)
588+
setIsEditingLabel(true)
589+
// Focus the input after render
590+
setTimeout(() => labelInputRef.current?.focus(), 0)
591+
}
592+
593+
const handleSaveLabel = () => {
594+
const trimmedValue = editingLabelValue.trim()
595+
if (trimmedValue && trimmedValue !== data.label) {
596+
setNodes((nodes) =>
597+
nodes.map((n) =>
598+
n.id === id
599+
? { ...n, data: { ...n.data, label: trimmedValue } }
600+
: n
601+
)
602+
)
603+
markDirty()
604+
}
605+
setIsEditingLabel(false)
606+
}
607+
608+
const handleCancelEditing = () => {
609+
setIsEditingLabel(false)
610+
}
611+
612+
const handleLabelKeyDown = (e: React.KeyboardEvent) => {
613+
if (e.key === 'Enter') {
614+
e.preventDefault()
615+
handleSaveLabel()
616+
} else if (e.key === 'Escape') {
617+
handleCancelEditing()
618+
}
619+
}
576620

577621
// Check if there are unfilled required parameters or inputs
578622
const componentParameters = component.parameters ?? []
@@ -851,7 +895,34 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {
851895
<div className="flex-1 min-w-0">
852896
<div className="flex items-center justify-between gap-2">
853897
<div className="min-w-0">
854-
<h3 className="text-sm font-semibold truncate">{displayLabel}</h3>
898+
{isEditingLabel ? (
899+
<input
900+
ref={labelInputRef}
901+
type="text"
902+
value={editingLabelValue}
903+
onChange={(e) => setEditingLabelValue(e.target.value)}
904+
onBlur={handleSaveLabel}
905+
onKeyDown={handleLabelKeyDown}
906+
className="text-sm font-semibold bg-transparent border-b border-primary outline-none w-full py-0"
907+
autoFocus
908+
/>
909+
) : (
910+
<div
911+
className={cn(
912+
"group/label",
913+
!isEntryPoint && mode === 'design' && "cursor-text"
914+
)}
915+
onDoubleClick={handleStartEditing}
916+
title={!isEntryPoint && mode === 'design' ? "Double-click to rename" : undefined}
917+
>
918+
<h3 className="text-sm font-semibold truncate">{displayLabel}</h3>
919+
{hasCustomLabel && (
920+
<span className="text-[10px] text-muted-foreground opacity-70 truncate block">
921+
{component.name}
922+
</span>
923+
)}
924+
</div>
925+
)}
855926
</div>
856927
<div className="flex items-center gap-1">
857928
{/* Delete button (Design Mode only, not Entry Point) */}

0 commit comments

Comments
 (0)