feat(network): add inline label editing on DeviceNode (double-click)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react'
|
import { memo, useState, useRef, useEffect } from 'react'
|
||||||
import { Position, NodeResizer, type NodeProps } from '@xyflow/react'
|
import { Position, NodeResizer, useReactFlow, type NodeProps } from '@xyflow/react'
|
||||||
import { BaseNode, BaseNodeHeader, BaseNodeHeaderTitle, BaseNodeContent } from '../ui/base-node'
|
import { BaseNode, BaseNodeHeader, BaseNodeContent } from '../ui/base-node'
|
||||||
import { BaseHandle } from '../ui/base-handle'
|
import { BaseHandle } from '../ui/base-handle'
|
||||||
import { NodeStatusIndicator, type NodeStatus } from '../ui/node-status-indicator'
|
import { NodeStatusIndicator, type NodeStatus } from '../ui/node-status-indicator'
|
||||||
import { NodeTooltip, NodeTooltipTrigger, NodeTooltipContent } from '../ui/node-tooltip'
|
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_MIN = 80 // minimum square side in px
|
||||||
const NODE_MAX = 280 // maximum 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 nodeData = data as unknown as DeviceNodeData
|
||||||
const { icon: Icon, color } = getDeviceRenderConfig(nodeData.deviceType, nodeData.category)
|
const { icon: Icon, color } = getDeviceRenderConfig(nodeData.deviceType, nodeData.category)
|
||||||
const status = (nodeData.properties?.status || 'unknown') as NodeStatus
|
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]
|
// IP font: 9px at default, clamped to [8, 16]
|
||||||
const ipPx = Math.max(8, Math.min(16, Math.round(scale * 9)))
|
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<HTMLInputElement>(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
|
const hasTooltipContent = props.hostname || props.ip || props.vendor || props.model || props.role || props.notes
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,9 +84,40 @@ function DeviceNodeComponent({ data, selected, width, height }: NodeProps) {
|
|||||||
<BaseNode className="w-full h-full group flex flex-col items-center justify-center">
|
<BaseNode className="w-full h-full group flex flex-col items-center justify-center">
|
||||||
<BaseNodeHeader className="flex-col gap-1 items-center py-2 px-2">
|
<BaseNodeHeader className="flex-col gap-1 items-center py-2 px-2">
|
||||||
<Icon size={iconPx} style={{ color }} />
|
<Icon size={iconPx} style={{ color }} />
|
||||||
<BaseNodeHeaderTitle className="text-center leading-tight" style={{ fontSize: labelPx }}>
|
{editing ? (
|
||||||
{nodeData.label}
|
<input
|
||||||
</BaseNodeHeaderTitle>
|
ref={inputRef}
|
||||||
|
value={labelValue}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
style={{ fontSize: labelPx }}
|
||||||
|
className="font-medium text-primary text-center leading-tight line-clamp-2 cursor-default"
|
||||||
|
onDoubleClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setEditing(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelValue}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</BaseNodeHeader>
|
</BaseNodeHeader>
|
||||||
{ip && (
|
{ip && (
|
||||||
<BaseNodeContent className="items-center pt-0 pb-1">
|
<BaseNodeContent className="items-center pt-0 pb-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user