Assign Agent no longer strands you at “Repository not linked to any board.”

This commit is contained in:
null 2026-05-24 19:52:50 -05:00
parent f71e0767a4
commit 086fc8fc49
1 changed files with 104 additions and 8 deletions

View File

@ -8,6 +8,10 @@ import {
useListAgentsApiV1AgentsGet,
type listAgentsApiV1AgentsGetResponse,
} from "@/api/generated/agents/agents";
import {
useListBoardsApiV1BoardsGet,
type listBoardsApiV1BoardsGetResponse,
} from "@/api/generated/boards/boards";
import { ApiError } from "@/api/mutator";
import {
Dialog,
@ -20,7 +24,11 @@ import {
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import type { ForgejoIssue, AssignIssueAgentResponse } from "@/lib/api-forgejo";
import { assignIssueToAgent, getLinkedBoardsForRepository } from "@/lib/api-forgejo";
import {
assignIssueToAgent,
getLinkedBoardsForRepository,
linkBoardForgejoRepository,
} from "@/lib/api-forgejo";
type LinkedBoard = { id: string; name: string };
@ -74,6 +82,8 @@ export function AssignIssueAgentDialog({
const [linkedBoards, setLinkedBoards] = useState<LinkedBoard[]>([]);
const [boardsLoading, setBoardsLoading] = useState(false);
const [linkedBoardsLoaded, setLinkedBoardsLoaded] = useState(false);
const [linkBoardId, setLinkBoardId] = useState("");
const [isLinkingRepository, setIsLinkingRepository] = useState(false);
// Fetch only boards linked to this issue's repository — prevents picking a
// board that the backend will reject with "not linked to any board".
@ -110,10 +120,31 @@ export function AssignIssueAgentDialog({
{ query: { enabled: open && !!boardId, refetchOnMount: "always" } },
);
const boardsQuery = useListBoardsApiV1BoardsGet<
listBoardsApiV1BoardsGetResponse,
ApiError
>(
{ limit: 200 },
{
query: {
enabled:
open &&
linkedBoardsLoaded &&
!boardsLoading &&
linkedBoards.length === 0,
refetchOnMount: "always",
},
},
);
const agents =
agentsQuery.data?.status === 200
? (agentsQuery.data.data.items ?? [])
: [];
const allBoards =
boardsQuery.data?.status === 200
? (boardsQuery.data.data.items ?? [])
: [];
useEffect(() => {
if (!open) {
@ -124,6 +155,8 @@ export function AssignIssueAgentDialog({
setStartImmediately(true);
setError(null);
setLinkedBoardsLoaded(false);
setLinkBoardId("");
setIsLinkingRepository(false);
}
}, [open]);
@ -164,6 +197,32 @@ export function AssignIssueAgentDialog({
}
};
const handleLinkRepositoryToBoard = async () => {
if (!issue || !linkBoardId) return;
setIsLinkingRepository(true);
setError(null);
try {
await linkBoardForgejoRepository(linkBoardId, issue.repository_id);
const linkedBoard = allBoards.find((board) => board.id === linkBoardId);
const nextBoard = {
id: linkBoardId,
name: linkedBoard?.name ?? "Selected board",
};
setLinkedBoards([nextBoard]);
setBoardId(nextBoard.id);
setLinkBoardId("");
} catch (err) {
setError(
err instanceof Error
? err.message
: "Failed to link repository to board",
);
} finally {
setIsLinkingRepository(false);
}
};
const toggleSwitch = (
value: boolean,
setter: (v: boolean) => void,
@ -227,13 +286,50 @@ export function AssignIssueAgentDialog({
use its Git Project repositories panel, or manage tracked
repositories first.
</p>
<Link
href="/git-projects/repositories"
className="mt-3 inline-flex h-9 items-center gap-2 rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 text-xs font-semibold text-strong transition hover:border-[color:var(--accent)] hover:text-[color:var(--accent)]"
>
Manage Git repositories
<ExternalLink className="h-3.5 w-3.5" />
</Link>
<div className="mt-4 space-y-2">
<label className="text-sm font-medium text-strong">
Link to board <span className="text-[color:var(--danger)]">*</span>
</label>
<select
value={linkBoardId}
onChange={(e) => setLinkBoardId(e.target.value)}
disabled={boardsQuery.isLoading || isLinkingRepository}
className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
>
<option value="">
{boardsQuery.isLoading ? "Loading boards..." : "Select a board..."}
</option>
{allBoards.map((board) => (
<option key={board.id} value={board.id}>
{board.name}
</option>
))}
</select>
{boardsQuery.isError ? (
<p className="text-xs text-[color:var(--danger)]">
Unable to load boards. You can still manage repositories
from Git Projects.
</p>
) : null}
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button
size="sm"
onClick={handleLinkRepositoryToBoard}
disabled={
!linkBoardId || boardsQuery.isLoading || isLinkingRepository
}
>
{isLinkingRepository ? "Linking..." : "Link and continue"}
</Button>
<Link
href="/git-projects/repositories"
className="inline-flex h-9 items-center gap-2 rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 text-xs font-semibold text-strong transition hover:border-[color:var(--accent)] hover:text-[color:var(--accent)]"
>
Manage Git repositories
<ExternalLink className="h-3.5 w-3.5" />
</Link>
</div>
</div>
) : (
<>