From 9786c6b1fb1607bde10667d12fe18bd74baebde0 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 13 Apr 2026 20:17:41 +0000 Subject: [PATCH] feat(network): add inline label editing on DeviceNode (double-click) Co-Authored-By: Claude Sonnet 4.6 --- .../components/network/nodes/DeviceNode.tsx | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/network/nodes/DeviceNode.tsx b/frontend/src/components/network/nodes/DeviceNode.tsx index 30b9faf6..d0171849 100644 --- a/frontend/src/components/network/nodes/DeviceNode.tsx +++ b/frontend/src/components/network/nodes/DeviceNode.tsx @@ -1,6 +1,6 @@ -import { memo } from 'react' -import { Position, NodeResizer, type NodeProps } from '@xyflow/react' -import { BaseNode, BaseNodeHeader, BaseNodeHeaderTitle, BaseNodeContent } from '../ui/base-node' +import { memo, useState, useRef, useEffect } from 'react' +import { Position, NodeResizer, useReactFlow, type NodeProps } from '@xyflow/react' +import { BaseNode, BaseNodeHeader, BaseNodeContent } from '../ui/base-node' import { BaseHandle } from '../ui/base-handle' import { NodeStatusIndicator, type NodeStatus } from '../ui/node-status-indicator' import { NodeTooltip, NodeTooltipTrigger, NodeTooltipContent } from '../ui/node-tooltip' @@ -29,7 +29,7 @@ const NODE_DEFAULT = 120 // default square side in px const NODE_MIN = 80 // minimum square side in px const NODE_MAX = 280 // maximum square side in px -function DeviceNodeComponent({ data, selected, width, height }: NodeProps) { +function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) { const nodeData = data as unknown as DeviceNodeData const { icon: Icon, color } = getDeviceRenderConfig(nodeData.deviceType, nodeData.category) const status = (nodeData.properties?.status || 'unknown') as NodeStatus @@ -47,6 +47,23 @@ function DeviceNodeComponent({ data, selected, width, height }: NodeProps) { // IP font: 9px at default, clamped to [8, 16] const ipPx = Math.max(8, Math.min(16, Math.round(scale * 9))) + const [editing, setEditing] = useState(false) + const [labelValue, setLabelValue] = useState(nodeData.label ?? '') + const inputRef = useRef(null) + const { updateNodeData } = useReactFlow() + + useEffect(() => { + if (editing) { + inputRef.current?.focus() + inputRef.current?.select() + } + }, [editing]) + + // Sync if data.label changes externally (e.g. undo/redo) + useEffect(() => { + if (!editing) setLabelValue(nodeData.label ?? '') + }, [nodeData.label, editing]) + const hasTooltipContent = props.hostname || props.ip || props.vendor || props.model || props.role || props.notes return ( @@ -67,9 +84,40 @@ function DeviceNodeComponent({ data, selected, width, height }: NodeProps) { - - {nodeData.label} - + {editing ? ( + setLabelValue(e.target.value)} + onBlur={() => { + setEditing(false) + if (labelValue !== nodeData.label) { + updateNodeData(id, { ...nodeData, label: labelValue }) + } + }} + onKeyDown={e => { + if (e.key === 'Enter') inputRef.current?.blur() + if (e.key === 'Escape') { + setLabelValue(nodeData.label ?? '') + setEditing(false) + } + e.stopPropagation() + }} + style={{ fontSize: labelPx }} + className="bg-transparent border-none outline-none text-center text-primary font-medium w-4/5" + /> + ) : ( + { + e.stopPropagation() + setEditing(true) + }} + > + {labelValue} + + )} {ip && (