feat: drop node onto container node to nest it

Dropping a top-level node over any container_mode node (proxmox,
docker_host, ...) now pops a confirm modal that nests it as a child
(sets parentId), mirroring the existing drop-onto-group flow.

- store: addToContainer(containerId, childId)
- CanvasContainer: detect container_mode intersection on drag stop
  (group still wins if both intersect)
- generalize ConfirmAddToGroupModal with a container variant

ha-relevant: yes
This commit is contained in:
Pouzor
2026-06-11 14:59:25 +02:00
parent b776b6b8f1
commit 2058e453ff
7 changed files with 221 additions and 17 deletions
+16 -2
View File
@@ -43,7 +43,7 @@ const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
const STANDALONE_STORAGE_KEY = 'homelable_canvas'
export default function App() {
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup } = useCanvasStore()
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer } = useCanvasStore()
const canvasRef = useRef<HTMLDivElement>(null)
const { isAuthenticated } = useAuthStore()
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
@@ -70,6 +70,7 @@ export default function App() {
const [editNodeId, setEditNodeId] = useState<string | null>(null)
const [pendingConnection, setPendingConnection] = useState<Connection | null>(null)
const [pendingGroupAdd, setPendingGroupAdd] = useState<{ nodeId: string; groupId: string } | null>(null)
const [pendingContainerAdd, setPendingContainerAdd] = useState<{ nodeId: string; containerId: string } | null>(null)
const [editEdgeId, setEditEdgeId] = useState<string | null>(null)
const [scanConfigOpen, setScanConfigOpen] = useState(false)
const [settingsOpen, setSettingsOpen] = useState(false)
@@ -634,6 +635,7 @@ export default function App() {
onNodeDoubleClick={handleNodeDoubleClick}
onNodeDragStart={snapshotHistory}
onRequestAddToGroup={setPendingGroupAdd}
onRequestAddToContainer={setPendingContainerAdd}
onOpenPending={(deviceId) => openPendingModal(deviceId)}
/>
</div>
@@ -810,7 +812,7 @@ export default function App() {
<ConfirmAddToGroupModal
open={!!pendingGroupAdd}
nodeLabel={pendingGroupAdd ? (nodes.find((n) => n.id === pendingGroupAdd.nodeId)?.data.label ?? '') : ''}
groupLabel={pendingGroupAdd ? (nodes.find((n) => n.id === pendingGroupAdd.groupId)?.data.label ?? '') : ''}
targetLabel={pendingGroupAdd ? (nodes.find((n) => n.id === pendingGroupAdd.groupId)?.data.label ?? '') : ''}
onConfirm={() => {
if (pendingGroupAdd) addToGroup(pendingGroupAdd.groupId, pendingGroupAdd.nodeId)
setPendingGroupAdd(null)
@@ -818,6 +820,18 @@ export default function App() {
onCancel={() => setPendingGroupAdd(null)}
/>
<ConfirmAddToGroupModal
open={!!pendingContainerAdd}
variant="container"
nodeLabel={pendingContainerAdd ? (nodes.find((n) => n.id === pendingContainerAdd.nodeId)?.data.label ?? '') : ''}
targetLabel={pendingContainerAdd ? (nodes.find((n) => n.id === pendingContainerAdd.containerId)?.data.label ?? '') : ''}
onConfirm={() => {
if (pendingContainerAdd) addToContainer(pendingContainerAdd.containerId, pendingContainerAdd.nodeId)
setPendingContainerAdd(null)
}}
onCancel={() => setPendingContainerAdd(null)}
/>
{!STANDALONE && (
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
)}