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 && (
+
+ )}
+
{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}