diff --git a/backend/app/api/forgejo_repositories.py b/backend/app/api/forgejo_repositories.py index 6f4bfe7..e5c6ba7 100644 --- a/backend/app/api/forgejo_repositories.py +++ b/backend/app/api/forgejo_repositories.py @@ -11,8 +11,11 @@ from sqlmodel import select from app.api.deps import require_org_member from app.db import crud from app.db.session import get_session +from app.models.boards import Board +from app.models.board_repository_links import BoardRepositoryLink from app.models.forgejo_connections import ForgejoConnection from app.models.forgejo_repositories import ForgejoRepository +from app.schemas.boards import BoardRead from app.schemas.common import OkResponse from app.schemas.forgejo_repositories import ( ForgejoRepositoryCreate, @@ -348,3 +351,38 @@ def _mask_repository(repository: ForgejoRepository, connection: ForgejoConnectio "created_at": repository.created_at, "updated_at": repository.updated_at, } + + +@router.get("/{repository_id}/boards", response_model=list[BoardRead]) +async def list_boards_for_repository( + repository_id: UUID, + session: AsyncSession = SESSION_DEP, + ctx: OrganizationContext = ORG_MEMBER_DEP, +) -> list[BoardRead]: + """Return all boards that have this repository linked to them.""" + repository = await crud.get_by_id(session, ForgejoRepository, repository_id) + if repository is None or repository.organization_id != ctx.organization.id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found") + + links = ( + await session.exec( + select(BoardRepositoryLink).where( + BoardRepositoryLink.organization_id == ctx.organization.id, + BoardRepositoryLink.repository_id == repository_id, + ) + ) + ).all() + + if not links: + return [] + + board_ids = [link.board_id for link in links] + boards = ( + await session.exec( + select(Board).where( + Board.id.in_(board_ids), + Board.organization_id == ctx.organization.id, + ) + ) + ).all() + return [BoardRead.model_validate(b) for b in boards] diff --git a/frontend/src/components/git/AssignIssueAgentDialog.tsx b/frontend/src/components/git/AssignIssueAgentDialog.tsx index 398319b..f28966d 100644 --- a/frontend/src/components/git/AssignIssueAgentDialog.tsx +++ b/frontend/src/components/git/AssignIssueAgentDialog.tsx @@ -2,10 +2,6 @@ import { useEffect, useState } from "react"; -import { - useListBoardsApiV1BoardsGet, - type listBoardsApiV1BoardsGetResponse, -} from "@/api/generated/boards/boards"; import { useListAgentsApiV1AgentsGet, type listAgentsApiV1AgentsGetResponse, @@ -22,7 +18,9 @@ import { import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import type { ForgejoIssue, AssignIssueAgentResponse } from "@/lib/api-forgejo"; -import { assignIssueToAgent } from "@/lib/api-forgejo"; +import { assignIssueToAgent, getLinkedBoardsForRepository } from "@/lib/api-forgejo"; + +type LinkedBoard = { id: string; name: string }; type AssignIssueAgentDialogProps = { issue: ForgejoIssue | null; @@ -46,11 +44,31 @@ export function AssignIssueAgentDialog({ const [startImmediately, setStartImmediately] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); + const [linkedBoards, setLinkedBoards] = useState([]); + const [boardsLoading, setBoardsLoading] = useState(false); - const boardsQuery = useListBoardsApiV1BoardsGet< - listBoardsApiV1BoardsGetResponse, - ApiError - >(undefined, { query: { enabled: open, refetchOnMount: "always" } }); + // Fetch only boards linked to this issue's repository — prevents picking a + // board that the backend will reject with "not linked to any board". + useEffect(() => { + if (!open || !issue) return; + let cancelled = false; + setBoardsLoading(true); + setLinkedBoards([]); + setBoardId(""); + getLinkedBoardsForRepository(issue.repository_id) + .then((boards) => { + if (cancelled) return; + setLinkedBoards(boards); + if (boards.length === 1) setBoardId(boards[0].id); + }) + .catch(() => { + if (!cancelled) setLinkedBoards([]); + }) + .finally(() => { + if (!cancelled) setBoardsLoading(false); + }); + return () => { cancelled = true; }; + }, [open, issue]); const agentsQuery = useListAgentsApiV1AgentsGet< listAgentsApiV1AgentsGetResponse, @@ -60,10 +78,6 @@ export function AssignIssueAgentDialog({ { query: { enabled: open && !!boardId, refetchOnMount: "always" } }, ); - const boards = - boardsQuery.data?.status === 200 - ? (boardsQuery.data.data.items ?? []) - : []; const agents = agentsQuery.data?.status === 200 ? (agentsQuery.data.data.items ?? []) @@ -80,18 +94,14 @@ export function AssignIssueAgentDialog({ } }, [open]); - useEffect(() => { - if (boards.length > 0 && !boardId) { - setBoardId(boards[0].id); - } - }, [boards, boardId]); - useEffect(() => { setAgentId(""); }, [boardId]); if (!issue) return null; + const noLinkedBoards = !boardsLoading && linkedBoards.length === 0; + const handleSubmit = async () => { if (!boardId) return; setIsSubmitting(true); @@ -126,7 +136,9 @@ export function AssignIssueAgentDialog({ onClick={() => !disabled && setter(!value)} disabled={disabled} className={`mt-0.5 inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition ${ - value ? "border-emerald-600 bg-emerald-600" : "border-[color:var(--border)] bg-[color:var(--surface-muted)]" + value + ? "border-emerald-600 bg-emerald-600" + : "border-[color:var(--border)] bg-[color:var(--surface-muted)]" } ${disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer"}`} > -
- - -
- -
- - -
- -
- - -
- -
- -