fix: save button now fully persists canvas to DB
Root cause: handleAddNode/handleEdgeConfirm only updated Zustand store, never creating records in the DB. canvasApi.save() only updated positions of existing nodes, so nothing survived a page refresh. Fix: save now sends the full canvas state (all nodes + edges + data) and the backend does a full sync — upsert incoming, delete anything removed. This means Save is the single source of truth: no need to call individual create/update/delete APIs for every drag or edit.
This commit is contained in:
@@ -23,12 +23,40 @@ async def load_canvas(db: AsyncSession = Depends(get_db), _: str = Depends(get_c
|
|||||||
|
|
||||||
@router.post("/save")
|
@router.post("/save")
|
||||||
async def save_canvas(body: CanvasSaveRequest, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def save_canvas(body: CanvasSaveRequest, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
||||||
# Update node positions from canvas
|
incoming_node_ids = {n.id for n in body.nodes}
|
||||||
for node_pos in body.node_positions:
|
incoming_edge_ids = {e.id for e in body.edges}
|
||||||
node = await db.get(Node, node_pos.id)
|
|
||||||
|
# Delete nodes removed from canvas
|
||||||
|
existing_nodes = (await db.execute(select(Node))).scalars().all()
|
||||||
|
for node in existing_nodes:
|
||||||
|
if node.id not in incoming_node_ids:
|
||||||
|
await db.delete(node)
|
||||||
|
|
||||||
|
# Delete edges removed from canvas
|
||||||
|
existing_edges = (await db.execute(select(Edge))).scalars().all()
|
||||||
|
for edge in existing_edges:
|
||||||
|
if edge.id not in incoming_edge_ids:
|
||||||
|
await db.delete(edge)
|
||||||
|
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
# Upsert nodes
|
||||||
|
for node_data in body.nodes:
|
||||||
|
node = await db.get(Node, node_data.id)
|
||||||
if node:
|
if node:
|
||||||
node.pos_x = node_pos.x
|
for field, value in node_data.model_dump().items():
|
||||||
node.pos_y = node_pos.y
|
setattr(node, field, value)
|
||||||
|
else:
|
||||||
|
db.add(Node(**node_data.model_dump()))
|
||||||
|
|
||||||
|
# Upsert edges
|
||||||
|
for edge_data in body.edges:
|
||||||
|
edge = await db.get(Edge, edge_data.id)
|
||||||
|
if edge:
|
||||||
|
for field, value in edge_data.model_dump().items():
|
||||||
|
setattr(edge, field, value)
|
||||||
|
else:
|
||||||
|
db.add(Edge(**edge_data.model_dump()))
|
||||||
|
|
||||||
# Upsert viewport
|
# Upsert viewport
|
||||||
state = await db.get(CanvasState, 1)
|
state = await db.get(CanvasState, 1)
|
||||||
|
|||||||
@@ -1,17 +1,42 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.schemas.edges import EdgeResponse
|
from app.schemas.edges import EdgeResponse
|
||||||
from app.schemas.nodes import NodeResponse
|
from app.schemas.nodes import NodeResponse
|
||||||
|
|
||||||
|
|
||||||
class NodePosition(BaseModel):
|
class NodeSave(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
x: float
|
type: str
|
||||||
y: float
|
label: str
|
||||||
|
hostname: str | None = None
|
||||||
|
ip: str | None = None
|
||||||
|
mac: str | None = None
|
||||||
|
os: str | None = None
|
||||||
|
status: str = "unknown"
|
||||||
|
check_method: str | None = None
|
||||||
|
check_target: str | None = None
|
||||||
|
services: list[Any] = []
|
||||||
|
notes: str | None = None
|
||||||
|
parent_id: str | None = None
|
||||||
|
pos_x: float = 0
|
||||||
|
pos_y: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeSave(BaseModel):
|
||||||
|
id: str
|
||||||
|
source: str
|
||||||
|
target: str
|
||||||
|
type: str = "ethernet"
|
||||||
|
label: str | None = None
|
||||||
|
vlan_id: int | None = None
|
||||||
|
speed: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CanvasSaveRequest(BaseModel):
|
class CanvasSaveRequest(BaseModel):
|
||||||
node_positions: list[NodePosition] = []
|
nodes: list[NodeSave] = []
|
||||||
|
edges: list[EdgeSave] = []
|
||||||
viewport: dict = {}
|
viewport: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+29
-5
@@ -36,15 +36,39 @@ export default function App() {
|
|||||||
// Declare handleSave before the Ctrl+S effect so it is in scope
|
// Declare handleSave before the Ctrl+S effect so it is in scope
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const nodePositions = nodes.map((n) => ({ id: n.id, x: n.position.x, y: n.position.y }))
|
const nodesToSave = nodes.map((n) => ({
|
||||||
await canvasApi.save({ node_positions: nodePositions, viewport: {} })
|
id: n.id,
|
||||||
|
type: n.data.type,
|
||||||
|
label: n.data.label,
|
||||||
|
hostname: n.data.hostname ?? null,
|
||||||
|
ip: n.data.ip ?? null,
|
||||||
|
mac: n.data.mac ?? null,
|
||||||
|
os: n.data.os ?? null,
|
||||||
|
status: n.data.status,
|
||||||
|
check_method: n.data.check_method ?? null,
|
||||||
|
check_target: n.data.check_target ?? null,
|
||||||
|
services: n.data.services ?? [],
|
||||||
|
notes: n.data.notes ?? null,
|
||||||
|
parent_id: n.data.parent_id ?? null,
|
||||||
|
pos_x: n.position.x,
|
||||||
|
pos_y: n.position.y,
|
||||||
|
}))
|
||||||
|
const edgesToSave = edges.map((e) => ({
|
||||||
|
id: e.id,
|
||||||
|
source: e.source,
|
||||||
|
target: e.target,
|
||||||
|
type: e.data?.type ?? 'ethernet',
|
||||||
|
label: e.data?.label ?? null,
|
||||||
|
vlan_id: e.data?.vlan_id ?? null,
|
||||||
|
speed: e.data?.speed ?? null,
|
||||||
|
}))
|
||||||
|
await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport: {} })
|
||||||
markSaved()
|
markSaved()
|
||||||
toast.success('Canvas saved')
|
toast.success('Canvas saved')
|
||||||
} catch {
|
} catch {
|
||||||
markSaved()
|
toast.error('Save failed')
|
||||||
toast.success('Canvas saved (local)')
|
|
||||||
}
|
}
|
||||||
}, [nodes, markSaved])
|
}, [nodes, edges, markSaved])
|
||||||
|
|
||||||
// Keep a ref so the keydown handler always calls the latest version
|
// Keep a ref so the keydown handler always calls the latest version
|
||||||
const handleSaveRef = useRef(handleSave)
|
const handleSaveRef = useRef(handleSave)
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ export const authApi = {
|
|||||||
|
|
||||||
export const canvasApi = {
|
export const canvasApi = {
|
||||||
load: () => api.get('/canvas'),
|
load: () => api.get('/canvas'),
|
||||||
save: (payload: { node_positions: { id: string; x: number; y: number }[]; viewport: object }) =>
|
save: (payload: {
|
||||||
api.post('/canvas/save', payload),
|
nodes: object[]
|
||||||
|
edges: object[]
|
||||||
|
viewport: object
|
||||||
|
}) => api.post('/canvas/save', payload),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nodesApi = {
|
export const nodesApi = {
|
||||||
|
|||||||
Reference in New Issue
Block a user