feat: drop new nodes at centre of visible canvas
New nodes previously landed at a fixed canvas origin, often off-screen, forcing the user to drag them into view. Add a projector (registered by CanvasContainer inside ReactFlowProvider) that maps the visible-canvas centre into flow coordinates, and use it for add-node, group rect, text, Zigbee/Z-Wave imports, and pending-device approval (single + bulk). ha-relevant: yes
This commit is contained in:
@@ -23,6 +23,7 @@ import { edgeTypes } from './edges/edgeTypes'
|
||||
import { SearchBar } from './SearchBar'
|
||||
import { AlignmentGuides } from './AlignmentGuides'
|
||||
import { useAlignmentGuides } from '@/hooks/useAlignmentGuides'
|
||||
import { setViewportCenterProjector } from '@/utils/viewportCenter'
|
||||
import type { NodeData, EdgeData } from '@/types'
|
||||
|
||||
interface CanvasContainerProps {
|
||||
@@ -52,6 +53,20 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o
|
||||
cursorRef.current = { x: e.clientX, y: e.clientY }
|
||||
}, [])
|
||||
|
||||
// Expose the visible-canvas centre (in flow coords) to add-node handlers that
|
||||
// live outside ReactFlowProvider, so new nodes land where the user is looking.
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
setViewportCenterProjector(() => {
|
||||
const rect = wrapperRef.current?.getBoundingClientRect()
|
||||
const screen = rect
|
||||
? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
||||
: { x: window.innerWidth / 2, y: window.innerHeight / 2 }
|
||||
return screenToFlowPosition(screen)
|
||||
})
|
||||
return () => setViewportCenterProjector(null)
|
||||
}, [screenToFlowPosition])
|
||||
|
||||
// Copy / paste shortcuts. Registered here (inside ReactFlowProvider) so paste
|
||||
// can project the cursor / viewport center into flow coordinates.
|
||||
useEffect(() => {
|
||||
@@ -146,7 +161,7 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o
|
||||
}, [onRequestAddToGroup, onRequestAddToContainer, getIntersectingNodes, onNodeDragStop])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full" style={{ background: theme.colors.canvasBackground }} onMouseMove={onMouseMove}>
|
||||
<div ref={wrapperRef} className="w-full h-full" style={{ background: theme.colors.canvasBackground }} onMouseMove={onMouseMove}>
|
||||
<ReactFlow
|
||||
nodes={visibleNodes}
|
||||
edges={visibleEdges}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { buildZigbeeProperties, isZigbeeType } from '@/utils/zigbeeProperties'
|
||||
import { buildZwaveProperties, isZwaveType } from '@/utils/zwaveProperties'
|
||||
import { buildMacProperty } from '@/utils/macProperty'
|
||||
import { formatRelative, formatTimestamp } from '@/utils/timeFormat'
|
||||
import { getCenteredPosition } from '@/utils/viewportCenter'
|
||||
|
||||
interface PendingDevicesModalProps {
|
||||
open: boolean
|
||||
@@ -294,7 +295,7 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
|
||||
addNode({
|
||||
id: nodeId,
|
||||
type: nodeData.type,
|
||||
position: { x: 400, y: 300 },
|
||||
position: getCenteredPosition(),
|
||||
data: { ...nodeData, status: wireless ? ('online' as const) : ('unknown' as const) },
|
||||
})
|
||||
injectAutoEdges(res.data.edges)
|
||||
@@ -336,6 +337,9 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
|
||||
const deviceToNode: Record<string, string> = {}
|
||||
res.data.device_ids.forEach((did, i) => { deviceToNode[did] = res.data.node_ids[i] })
|
||||
const approvedDevices = devices.filter((d) => ids.includes(d.id))
|
||||
const cols = Math.min(4, approvedDevices.length)
|
||||
const rows = Math.ceil(approvedDevices.length / 4)
|
||||
const origin = getCenteredPosition(cols * 160, rows * 100)
|
||||
approvedDevices.forEach((d, i) => {
|
||||
const nodeId = deviceToNode[d.id]
|
||||
if (!nodeId) return
|
||||
@@ -345,7 +349,7 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
|
||||
addNode({
|
||||
id: nodeId,
|
||||
type,
|
||||
position: { x: 400 + (i % 4) * 160, y: 300 + Math.floor(i / 4) * 100 },
|
||||
position: { x: origin.x + (i % 4) * 160, y: origin.y + Math.floor(i / 4) * 100 },
|
||||
data: {
|
||||
label: deviceLabel(d),
|
||||
type,
|
||||
|
||||
Reference in New Issue
Block a user