From 31e3b07d24ee61498ad640142c8361dbf3987490 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 20 May 2026 03:16:52 -0500 Subject: [PATCH] feat: #24 --- backend/app/api/forgejo_webhooks.py | 26 +++++++- frontend/src/app/boards/[boardId]/page.tsx | 3 +- .../git/BoardForgejoRepositoryLinks.tsx | 62 ++++++++++++------- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/backend/app/api/forgejo_webhooks.py b/backend/app/api/forgejo_webhooks.py index 1f5f3e2..c6092d3 100644 --- a/backend/app/api/forgejo_webhooks.py +++ b/backend/app/api/forgejo_webhooks.py @@ -236,6 +236,20 @@ def _assignees(issue_data: dict[str, Any]) -> Any: return assignees +def _milestone(issue_data: dict[str, Any]) -> dict[str, object] | None: + raw = issue_data.get("milestone") + if not isinstance(raw, dict): + return None + return { + "id": raw.get("id"), + "title": str(raw.get("title") or ""), + "state": str(raw.get("state") or "open"), + "description": raw.get("description") or None, + "due_on": raw.get("due_on") or None, + "closed_at": raw.get("closed_at") or None, + } + + def _author(issue_data: dict[str, Any]) -> str: user = issue_data.get("user") if isinstance(user, dict): @@ -292,17 +306,23 @@ async def _upsert_issue( else _parse_optional_iso_datetime(issue_data.get("closed_at")) or utcnow() ) + raw_body = issue_data.get("body") or "" + body_text = str(raw_body) if raw_body else None + milestone = _milestone(issue_data) + if existing is None: issue = ForgejoIssue( organization_id=repository.organization_id, repository_id=repository.id, forgejo_issue_number=number, title=str(issue_data.get("title") or ""), - body_preview=str(issue_data.get("body") or "")[:1000], + body=body_text, + body_preview=str(raw_body)[:1000] if raw_body else None, state=state, is_pull_request=False, labels=_labels(issue_data), assignees=_assignees(issue_data), + milestone=milestone, author=_author(issue_data), html_url=str(issue_data.get("html_url") or ""), forgejo_created_at=_parse_iso_datetime(issue_data.get("created_at")), @@ -314,7 +334,9 @@ async def _upsert_issue( return issue, True existing.title = str(issue_data.get("title") or "") - existing.body_preview = str(issue_data.get("body") or "")[:1000] + existing.body = body_text + existing.body_preview = str(raw_body)[:1000] if raw_body else None + existing.milestone = milestone existing.state = state existing.is_pull_request = False existing.labels = _labels(issue_data) diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 42017c5..32a7615 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -843,6 +843,7 @@ export default function BoardDetailPage() { membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; return resolveMemberDisplayName(member, DEFAULT_HUMAN_LABEL); }, [membershipQuery.data]); + const canRead = boardAccess.canRead; const canWrite = boardAccess.canWrite; const [board, setBoard] = useState(null); @@ -3243,7 +3244,7 @@ export default function BoardDetailPage() { - {canWrite && boardId ? ( + {canRead && boardId ? (
diff --git a/frontend/src/components/git/BoardForgejoRepositoryLinks.tsx b/frontend/src/components/git/BoardForgejoRepositoryLinks.tsx index fe26024..beaa827 100644 --- a/frontend/src/components/git/BoardForgejoRepositoryLinks.tsx +++ b/frontend/src/components/git/BoardForgejoRepositoryLinks.tsx @@ -22,8 +22,8 @@ interface BoardForgejoRepositoryLinksProps { canWrite?: boolean; } -type LinkedRepository = BoardForgejoRepositoryLink & { - repository: ForgejoRepository; +type LinkedRepository = Omit & { + repository: ForgejoRepository | null; }; const normalizeBoardLinks = ( @@ -44,6 +44,10 @@ export function BoardForgejoRepositoryLinks({ const [allRepos, setAllRepos] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [isLoading, setIsLoading] = useState(true); + const [linkedLoadError, setLinkedLoadError] = useState(null); + const [availableLoadError, setAvailableLoadError] = useState( + null, + ); const [linkError, setLinkError] = useState(null); const [unlinkError, setUnlinkError] = useState(null); const [isLinking, setIsLinking] = useState(false); @@ -56,13 +60,13 @@ export function BoardForgejoRepositoryLinks({ try { const result = await getBoardForgejoRepositories(boardId); setLinkedLinks(normalizeBoardLinks(result)); - setLinkError(null); + setLinkedLoadError(null); } catch (err) { const message = err instanceof Error ? err.message : "Unable to load linked Git Project repositories."; - setLinkError(message); + setLinkedLoadError(message); } }, [boardId]); @@ -70,13 +74,13 @@ export function BoardForgejoRepositoryLinks({ try { const repos = await getForgejoRepositories(); setAllRepos(repos); - setLinkError(null); + setAvailableLoadError(null); } catch (err) { const message = err instanceof Error ? err.message : "Unable to load available Git Project repositories."; - setLinkError(message); + setAvailableLoadError(message); } }, []); @@ -110,14 +114,10 @@ export function BoardForgejoRepositoryLinks({ const linkedRepos = useMemo( () => - linkedLinks - .map((link) => ({ - ...link, - repository: link.repository ?? repoById.get(link.repository_id), - })) - .filter( - (link): link is LinkedRepository => link.repository !== undefined, - ), + linkedLinks.map((link) => ({ + ...link, + repository: link.repository ?? repoById.get(link.repository_id) ?? null, + })), [linkedLinks, repoById], ); @@ -201,10 +201,17 @@ export function BoardForgejoRepositoryLinks({

- {linkedRepos.length} linked + {linkedLinks.length} linked + {linkedLoadError && ( +
+ + {linkedLoadError} +
+ )} + {linkError && (
@@ -241,6 +248,12 @@ export function BoardForgejoRepositoryLinks({
{linkedRepos.map((link) => { const repository = link.repository; + const displayName = repository + ? repositoryDisplayName(repository) + : link.repository_id; + const subtitle = repository + ? `${repository.owner}/${repository.repo}` + : "Repository details unavailable"; return (

- {repositoryDisplayName(repository)} + {displayName}

- {repository.owner}/{repository.repo} + {subtitle}

{canWrite ? ( @@ -271,7 +284,7 @@ export function BoardForgejoRepositoryLinks({ setUnlinkError(null); setUnlinkTarget(link); }} - aria-label={`Unlink ${repositoryDisplayName(repository)}`} + aria-label={`Unlink ${displayName}`} > @@ -303,6 +316,13 @@ export function BoardForgejoRepositoryLinks({ />
+ {availableLoadError ? ( +
+ + {availableLoadError} +
+ ) : null} + {availableRepos.length === 0 ? (

@@ -377,7 +397,7 @@ export function BoardForgejoRepositoryLinks({ title="Unlink Git Project repository" description={ unlinkTarget - ? `Remove "${repositoryDisplayName(unlinkTarget.repository)}" from this board? Issues from this repository will no longer appear on the board.` + ? `Remove "${unlinkTarget.repository ? repositoryDisplayName(unlinkTarget.repository) : unlinkTarget.repository_id}" from this board? Issues from this repository will no longer appear on the board.` : "Remove this repository from the board?" } onConfirm={handleUnlinkRepo}