feat: floor plan map, LQI edge coloring, zigbee path highlighting, eslint/test fixes, vite audit fix
This commit is contained in:
+43
-7
@@ -26,6 +26,7 @@ import { SettingsModal } from '@/components/modals/SettingsModal'
|
||||
import { ZigbeeImportModal } from '@/components/zigbee/ZigbeeImportModal'
|
||||
import { ZwaveImportModal } from '@/components/zwave/ZwaveImportModal'
|
||||
import { GroupRectModal, type GroupRectFormData } from '@/components/modals/GroupRectModal'
|
||||
import { FloorMapModal } from '@/components/modals/FloorMapModal'
|
||||
import { TextModal, type TextFormData } from '@/components/modals/TextModal'
|
||||
import { ThemeModal } from '@/components/modals/ThemeModal'
|
||||
import { SearchModal } from '@/components/modals/SearchModal'
|
||||
@@ -41,14 +42,14 @@ import { canvasApi, designsApi, liveviewApi } from '@/api/client'
|
||||
import * as standaloneStorage from '@/utils/standaloneStorage'
|
||||
import { demoNodes, demoEdges } from '@/utils/demoData'
|
||||
import { useStatusPolling } from '@/hooks/useStatusPolling'
|
||||
import type { NodeData, EdgeData, CustomStyleDef } from '@/types'
|
||||
import type { NodeData, EdgeData, CustomStyleDef, FloorMapConfig } from '@/types'
|
||||
import type { ZigbeeNode, ZigbeeEdge } from '@/components/zigbee/types'
|
||||
import type { ZwaveNode, ZwaveEdge } from '@/components/zwave/types'
|
||||
|
||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||
|
||||
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, addToContainer } = 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, floorMap, setFloorMap } = useCanvasStore()
|
||||
const canvasRef = useRef<HTMLDivElement>(null)
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
|
||||
@@ -82,6 +83,7 @@ export default function App() {
|
||||
const [exportModalOpen, setExportModalOpen] = useState(false)
|
||||
const [zigbeeImportOpen, setZigbeeImportOpen] = useState(false)
|
||||
const [zwaveImportOpen, setZwaveImportOpen] = useState(false)
|
||||
const [floorMapModalOpen, setFloorMapModalOpen] = useState(false)
|
||||
|
||||
// Declare handleSave before the Ctrl+S effect so it is in scope.
|
||||
// Returns true on success, false on failure — the design-switch effect relies
|
||||
@@ -91,14 +93,16 @@ export default function App() {
|
||||
const saveDesignId = designIdOverride ?? activeDesignId
|
||||
if (STANDALONE) {
|
||||
if (!saveDesignId) return false
|
||||
standaloneStorage.saveCanvas(saveDesignId, { nodes, edges, theme_id: activeTheme, custom_style: customStyle })
|
||||
standaloneStorage.saveCanvas(saveDesignId, { nodes, edges, theme_id: activeTheme, custom_style: customStyle, floorMap })
|
||||
markSaved()
|
||||
toast.success('Canvas saved')
|
||||
return true
|
||||
}
|
||||
const nodesToSave = nodes.map(serializeNode)
|
||||
const edgesToSave = edges.map(serializeEdge)
|
||||
await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport: { theme_id: activeTheme }, custom_style: customStyle, design_id: saveDesignId })
|
||||
const viewport: Record<string, unknown> = { theme_id: activeTheme }
|
||||
if (floorMap) viewport.floor_map = floorMap
|
||||
await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport, custom_style: customStyle, design_id: saveDesignId })
|
||||
markSaved()
|
||||
toast.success('Canvas saved')
|
||||
return true
|
||||
@@ -106,7 +110,7 @@ export default function App() {
|
||||
toast.error('Save failed')
|
||||
return false
|
||||
}
|
||||
}, [nodes, edges, markSaved, activeTheme, customStyle, activeDesignId])
|
||||
}, [nodes, edges, markSaved, activeTheme, customStyle, activeDesignId, floorMap])
|
||||
|
||||
// Keep a ref so the keydown handler always calls the latest version
|
||||
const handleSaveRef = useRef(handleSave)
|
||||
@@ -127,6 +131,8 @@ export default function App() {
|
||||
const savedTheme = res.data.viewport?.theme_id
|
||||
if (savedTheme) setTheme(savedTheme)
|
||||
if (res.data.custom_style) setCustomStyle(res.data.custom_style as CustomStyleDef)
|
||||
const savedFloorMap = res.data.viewport?.floor_map as FloorMapConfig | undefined
|
||||
if (savedFloorMap) setFloorMap(savedFloorMap)
|
||||
loadCanvas(rfNodes, rfEdges)
|
||||
} else {
|
||||
loadCanvas(demoNodes, demoEdges)
|
||||
@@ -134,7 +140,7 @@ export default function App() {
|
||||
} catch {
|
||||
loadCanvas(demoNodes, demoEdges)
|
||||
}
|
||||
}, [loadCanvas, setTheme, setCustomStyle])
|
||||
}, [loadCanvas, setTheme, setCustomStyle, setFloorMap])
|
||||
|
||||
// Standalone counterpart of loadCanvasFromApi — reads a design's canvas from
|
||||
// localStorage, falling back to the demo canvas when it has never been saved.
|
||||
@@ -143,11 +149,12 @@ export default function App() {
|
||||
if (saved && saved.nodes.length > 0) {
|
||||
if (saved.theme_id) setTheme(saved.theme_id)
|
||||
if (saved.custom_style) setCustomStyle(saved.custom_style)
|
||||
if (saved.floorMap) setFloorMap(saved.floorMap)
|
||||
loadCanvas(saved.nodes, saved.edges)
|
||||
} else {
|
||||
loadCanvas(demoNodes, demoEdges)
|
||||
}
|
||||
}, [loadCanvas, setTheme, setCustomStyle])
|
||||
}, [loadCanvas, setTheme, setCustomStyle, setFloorMap])
|
||||
|
||||
const loadDesignsAndCanvas = useCallback(async () => {
|
||||
if (STANDALONE) {
|
||||
@@ -518,6 +525,25 @@ export default function App() {
|
||||
}
|
||||
}, [activeDesignId])
|
||||
|
||||
const handleFloorMapSubmit = useCallback((config: FloorMapConfig) => {
|
||||
snapshotHistory()
|
||||
if (floorMap) {
|
||||
// Preserve position from existing config when editing
|
||||
setFloorMap({ ...floorMap, ...config })
|
||||
} else {
|
||||
setFloorMap(config)
|
||||
}
|
||||
setFloorMapModalOpen(false)
|
||||
toast.success(floorMap ? 'Floor plan updated' : 'Floor plan imported')
|
||||
}, [floorMap, setFloorMap, snapshotHistory])
|
||||
|
||||
const handleFloorMapRemove = useCallback(() => {
|
||||
snapshotHistory()
|
||||
setFloorMap(null)
|
||||
setFloorMapModalOpen(false)
|
||||
toast.success('Floor plan removed')
|
||||
}, [setFloorMap, snapshotHistory])
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
const el = canvasRef.current?.querySelector<HTMLElement>('.react-flow')
|
||||
if (!el) { toast.error('Canvas not ready'); return }
|
||||
@@ -697,6 +723,7 @@ export default function App() {
|
||||
onScan={() => setScanConfigOpen(true)}
|
||||
onZigbeeImport={() => setZigbeeImportOpen(true)}
|
||||
onZwaveImport={() => setZwaveImportOpen(true)}
|
||||
onFloorMap={() => setFloorMapModalOpen(true)}
|
||||
onSave={handleSave}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
onOpenHistory={() => setScanHistoryOpen(true)}
|
||||
@@ -833,6 +860,15 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<FloorMapModal
|
||||
key={floorMapModalOpen ? 'floor-map-open' : 'floor-map-closed'}
|
||||
open={floorMapModalOpen}
|
||||
onClose={() => setFloorMapModalOpen(false)}
|
||||
onSubmit={handleFloorMapSubmit}
|
||||
onRemove={handleFloorMapRemove}
|
||||
initial={floorMap}
|
||||
/>
|
||||
|
||||
<GroupRectModal
|
||||
open={addGroupRectOpen}
|
||||
onClose={() => setAddGroupRectOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user