phase 6
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user