12d527aad6
Make designs (canvases) fully user-manageable: create with a chosen name and icon, rename, change icon, and delete. Replaces the hardcoded "New Electrical Design" button with a generic "New Canvas" flow. - Add Design.icon column + migration that backfills legacy rows (electrical -> zap, others -> dashboard) - DesignModal: name input + curated lucide icon picker (create + edit) - Sidebar switcher gains per-canvas edit/delete; delete guards the last canvas and confirms - designStore: addDesign/updateDesign/removeDesign with active reassignment - Fix data loss on design switch: abort load when the save fails and keep unsaved edits; skip the save-old step when the previous canvas was deleted - designsApi create/update carry icon; design_type kept for back-compat Tests: backend design CRUD (icon + cascade + last-canvas guard), designStore actions, designIcons resolver, DesignModal create/edit/validation. ha-relevant: yes
82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import get_current_user
|
|
from app.db.database import get_db
|
|
from app.db.models import CanvasState, Design, Edge, Node
|
|
from app.schemas.designs import DesignCreate, DesignResponse, DesignUpdate
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("", response_model=list[DesignResponse])
|
|
async def list_designs(
|
|
db: AsyncSession = Depends(get_db),
|
|
_: str = Depends(get_current_user),
|
|
) -> list[DesignResponse]:
|
|
designs = (await db.execute(select(Design).order_by(Design.created_at))).scalars().all()
|
|
return [DesignResponse.model_validate(d) for d in designs]
|
|
|
|
|
|
@router.post("", response_model=DesignResponse, status_code=201)
|
|
async def create_design(
|
|
body: DesignCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: str = Depends(get_current_user),
|
|
) -> DesignResponse:
|
|
design = Design(name=body.name, design_type=body.design_type, icon=body.icon)
|
|
db.add(design)
|
|
await db.flush()
|
|
# Create empty canvas state for the new design
|
|
db.add(CanvasState(design_id=design.id))
|
|
await db.commit()
|
|
await db.refresh(design)
|
|
return DesignResponse.model_validate(design)
|
|
|
|
|
|
@router.put("/{design_id}", response_model=DesignResponse)
|
|
async def update_design(
|
|
design_id: str,
|
|
body: DesignUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: str = Depends(get_current_user),
|
|
) -> DesignResponse:
|
|
design = await db.get(Design, design_id)
|
|
if not design:
|
|
raise HTTPException(404, "Design not found")
|
|
if body.name is not None:
|
|
design.name = body.name
|
|
if body.icon is not None:
|
|
design.icon = body.icon
|
|
await db.commit()
|
|
await db.refresh(design)
|
|
return DesignResponse.model_validate(design)
|
|
|
|
|
|
@router.delete("/{design_id}", status_code=204)
|
|
async def delete_design(
|
|
design_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: str = Depends(get_current_user),
|
|
) -> None:
|
|
design = await db.get(Design, design_id)
|
|
if not design:
|
|
raise HTTPException(404, "Design not found")
|
|
# Count remaining designs — prevent deleting the last one
|
|
count = (await db.execute(select(Design))).scalars().all()
|
|
if len(count) <= 1:
|
|
raise HTTPException(400, "Cannot delete the only design")
|
|
# Delete associated canvas state, edges, nodes
|
|
cs = await db.get(CanvasState, design_id)
|
|
if cs:
|
|
await db.delete(cs)
|
|
edges = (await db.execute(select(Edge).where(Edge.design_id == design_id))).scalars().all()
|
|
for e in edges:
|
|
await db.delete(e)
|
|
nodes = (await db.execute(select(Node).where(Node.design_id == design_id))).scalars().all()
|
|
for n in nodes:
|
|
await db.delete(n)
|
|
await db.delete(design)
|
|
await db.commit()
|