This commit is contained in:
Danilo Reyes
2025-11-02 14:03:01 -06:00
parent f85ae4d417
commit 3700ba02ea
14 changed files with 3103 additions and 19 deletions

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from app.boards.repository import BoardRepository
from app.boards.schemas import BoardCreate, BoardDetail, BoardSummary, BoardUpdate
from app.boards.schemas import BoardCreate, BoardDetail, BoardSummary, BoardUpdate, ViewportStateUpdate
from app.core.deps import get_current_user, get_db
from app.database.models.user import User
@@ -152,6 +152,48 @@ def update_board(
return BoardDetail.model_validate(board)
@router.patch("/{board_id}/viewport", status_code=status.HTTP_204_NO_CONTENT)
def update_viewport(
board_id: UUID,
viewport_data: ViewportStateUpdate,
current_user: Annotated[User, Depends(get_current_user)],
db: Annotated[Session, Depends(get_db)],
):
"""
Update board viewport state only (optimized for frequent updates).
This endpoint is designed for high-frequency viewport state updates
(debounced pan/zoom/rotate changes) with minimal overhead.
Args:
board_id: Board UUID
viewport_data: Viewport state data
current_user: Current authenticated user
db: Database session
Raises:
HTTPException: 404 if board not found or not owned by user
"""
repo = BoardRepository(db)
# Convert viewport data to dict
viewport_dict = viewport_data.model_dump()
board = repo.update_board(
board_id=board_id,
user_id=current_user.id,
title=None,
description=None,
viewport_state=viewport_dict,
)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Board {board_id} not found",
)
@router.delete("/{board_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_board(
board_id: UUID,

View File

@@ -14,6 +14,7 @@ from app.images.repository import ImageRepository
from app.images.schemas import (
BoardImageCreate,
BoardImageResponse,
BoardImageUpdate,
ImageListResponse,
ImageResponse,
ImageUploadResponse,
@@ -277,6 +278,52 @@ async def add_image_to_board(
return board_image
@router.patch("/boards/{board_id}/images/{image_id}", response_model=BoardImageResponse)
async def update_board_image(
board_id: UUID,
image_id: UUID,
data: BoardImageUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
Update board image position, transformations, z-order, or group.
This endpoint is optimized for frequent position updates (debounced from frontend).
Only provided fields are updated.
"""
# Verify board ownership
from sqlalchemy import select
board_result = await db.execute(select(Board).where(Board.id == board_id))
board = board_result.scalar_one_or_none()
if not board:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found")
if board.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
# Update board image
repo = ImageRepository(db)
board_image = await repo.update_board_image(
board_id=board_id,
image_id=image_id,
position=data.position,
transformations=data.transformations,
z_order=data.z_order,
group_id=data.group_id,
)
if not board_image:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not on this board")
# Load image relationship for response
await db.refresh(board_image, ["image"])
return board_image
@router.delete("/boards/{board_id}/images/{image_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_image_from_board(
board_id: UUID,

View File

@@ -22,6 +22,15 @@ class BoardCreate(BaseModel):
description: str | None = Field(default=None, description="Optional board description")
class ViewportStateUpdate(BaseModel):
"""Schema for updating viewport state only."""
x: float = Field(..., description="Horizontal pan position")
y: float = Field(..., description="Vertical pan position")
zoom: float = Field(..., ge=0.1, le=5.0, description="Zoom level (0.1 to 5.0)")
rotation: float = Field(..., ge=0, le=360, description="Canvas rotation in degrees (0 to 360)")
class BoardUpdate(BaseModel):
"""Schema for updating board metadata."""

View File

@@ -83,6 +83,23 @@ class BoardImageCreate(BaseModel):
return v
class BoardImageUpdate(BaseModel):
"""Schema for updating board image position/transformations."""
position: dict[str, float] | None = Field(None, description="Canvas position")
transformations: dict[str, Any] | None = Field(None, description="Image transformations")
z_order: int | None = Field(None, description="Layer order")
group_id: UUID | None = Field(None, description="Group membership")
@field_validator("position")
@classmethod
def validate_position(cls, v: dict[str, float] | None) -> dict[str, float] | None:
"""Validate position has x and y if provided."""
if v is not None and ("x" not in v or "y" not in v):
raise ValueError("Position must contain 'x' and 'y' coordinates")
return v
class BoardImageResponse(BaseModel):
"""Response for board image with all metadata."""