This commit is contained in:
null 2026-05-20 03:16:52 -05:00
parent 9ff87b7e20
commit 31e3b07d24
3 changed files with 67 additions and 24 deletions

View File

@ -236,6 +236,20 @@ def _assignees(issue_data: dict[str, Any]) -> Any:
return assignees 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: def _author(issue_data: dict[str, Any]) -> str:
user = issue_data.get("user") user = issue_data.get("user")
if isinstance(user, dict): if isinstance(user, dict):
@ -292,17 +306,23 @@ async def _upsert_issue(
else _parse_optional_iso_datetime(issue_data.get("closed_at")) or utcnow() 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: if existing is None:
issue = ForgejoIssue( issue = ForgejoIssue(
organization_id=repository.organization_id, organization_id=repository.organization_id,
repository_id=repository.id, repository_id=repository.id,
forgejo_issue_number=number, forgejo_issue_number=number,
title=str(issue_data.get("title") or ""), 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, state=state,
is_pull_request=False, is_pull_request=False,
labels=_labels(issue_data), labels=_labels(issue_data),
assignees=_assignees(issue_data), assignees=_assignees(issue_data),
milestone=milestone,
author=_author(issue_data), author=_author(issue_data),
html_url=str(issue_data.get("html_url") or ""), html_url=str(issue_data.get("html_url") or ""),
forgejo_created_at=_parse_iso_datetime(issue_data.get("created_at")), forgejo_created_at=_parse_iso_datetime(issue_data.get("created_at")),
@ -314,7 +334,9 @@ async def _upsert_issue(
return issue, True return issue, True
existing.title = str(issue_data.get("title") or "") 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.state = state
existing.is_pull_request = False existing.is_pull_request = False
existing.labels = _labels(issue_data) existing.labels = _labels(issue_data)

View File

@ -843,6 +843,7 @@ export default function BoardDetailPage() {
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
return resolveMemberDisplayName(member, DEFAULT_HUMAN_LABEL); return resolveMemberDisplayName(member, DEFAULT_HUMAN_LABEL);
}, [membershipQuery.data]); }, [membershipQuery.data]);
const canRead = boardAccess.canRead;
const canWrite = boardAccess.canWrite; const canWrite = boardAccess.canWrite;
const [board, setBoard] = useState<Board | null>(null); const [board, setBoard] = useState<Board | null>(null);
@ -3243,7 +3244,7 @@ export default function BoardDetailPage() {
</div> </div>
</div> </div>
{canWrite && boardId ? ( {canRead && boardId ? (
<div className="w-full"> <div className="w-full">
<BoardForgejoRepositoryLinks boardId={boardId} canWrite={canWrite} /> <BoardForgejoRepositoryLinks boardId={boardId} canWrite={canWrite} />
</div> </div>

View File

@ -22,8 +22,8 @@ interface BoardForgejoRepositoryLinksProps {
canWrite?: boolean; canWrite?: boolean;
} }
type LinkedRepository = BoardForgejoRepositoryLink & { type LinkedRepository = Omit<BoardForgejoRepositoryLink, "repository"> & {
repository: ForgejoRepository; repository: ForgejoRepository | null;
}; };
const normalizeBoardLinks = ( const normalizeBoardLinks = (
@ -44,6 +44,10 @@ export function BoardForgejoRepositoryLinks({
const [allRepos, setAllRepos] = useState<ForgejoRepository[]>([]); const [allRepos, setAllRepos] = useState<ForgejoRepository[]>([]);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [linkedLoadError, setLinkedLoadError] = useState<string | null>(null);
const [availableLoadError, setAvailableLoadError] = useState<string | null>(
null,
);
const [linkError, setLinkError] = useState<string | null>(null); const [linkError, setLinkError] = useState<string | null>(null);
const [unlinkError, setUnlinkError] = useState<string | null>(null); const [unlinkError, setUnlinkError] = useState<string | null>(null);
const [isLinking, setIsLinking] = useState(false); const [isLinking, setIsLinking] = useState(false);
@ -56,13 +60,13 @@ export function BoardForgejoRepositoryLinks({
try { try {
const result = await getBoardForgejoRepositories(boardId); const result = await getBoardForgejoRepositories(boardId);
setLinkedLinks(normalizeBoardLinks(result)); setLinkedLinks(normalizeBoardLinks(result));
setLinkError(null); setLinkedLoadError(null);
} catch (err) { } catch (err) {
const message = const message =
err instanceof Error err instanceof Error
? err.message ? err.message
: "Unable to load linked Git Project repositories."; : "Unable to load linked Git Project repositories.";
setLinkError(message); setLinkedLoadError(message);
} }
}, [boardId]); }, [boardId]);
@ -70,13 +74,13 @@ export function BoardForgejoRepositoryLinks({
try { try {
const repos = await getForgejoRepositories(); const repos = await getForgejoRepositories();
setAllRepos(repos); setAllRepos(repos);
setLinkError(null); setAvailableLoadError(null);
} catch (err) { } catch (err) {
const message = const message =
err instanceof Error err instanceof Error
? err.message ? err.message
: "Unable to load available Git Project repositories."; : "Unable to load available Git Project repositories.";
setLinkError(message); setAvailableLoadError(message);
} }
}, []); }, []);
@ -110,14 +114,10 @@ export function BoardForgejoRepositoryLinks({
const linkedRepos = useMemo( const linkedRepos = useMemo(
() => () =>
linkedLinks linkedLinks.map((link) => ({
.map((link) => ({ ...link,
...link, repository: link.repository ?? repoById.get(link.repository_id) ?? null,
repository: link.repository ?? repoById.get(link.repository_id), })),
}))
.filter(
(link): link is LinkedRepository => link.repository !== undefined,
),
[linkedLinks, repoById], [linkedLinks, repoById],
); );
@ -201,10 +201,17 @@ export function BoardForgejoRepositoryLinks({
</p> </p>
</div> </div>
<Badge variant="outline" className="w-fit"> <Badge variant="outline" className="w-fit">
{linkedRepos.length} linked {linkedLinks.length} linked
</Badge> </Badge>
</div> </div>
{linkedLoadError && (
<div className="mb-4 flex items-start gap-2 rounded-lg border border-[color:var(--danger)]/35 bg-[color:var(--danger-soft)] px-3 py-2 text-sm text-[color:var(--danger)]">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{linkedLoadError}</span>
</div>
)}
{linkError && ( {linkError && (
<div className="mb-4 flex items-start gap-2 rounded-lg border border-[color:var(--danger)]/35 bg-[color:var(--danger-soft)] px-3 py-2 text-sm text-[color:var(--danger)]"> <div className="mb-4 flex items-start gap-2 rounded-lg border border-[color:var(--danger)]/35 bg-[color:var(--danger-soft)] px-3 py-2 text-sm text-[color:var(--danger)]">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" /> <AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
@ -241,6 +248,12 @@ export function BoardForgejoRepositoryLinks({
<div className="grid gap-2 sm:grid-cols-2"> <div className="grid gap-2 sm:grid-cols-2">
{linkedRepos.map((link) => { {linkedRepos.map((link) => {
const repository = link.repository; const repository = link.repository;
const displayName = repository
? repositoryDisplayName(repository)
: link.repository_id;
const subtitle = repository
? `${repository.owner}/${repository.repo}`
: "Repository details unavailable";
return ( return (
<div <div
@ -250,15 +263,15 @@ export function BoardForgejoRepositoryLinks({
<div className="min-w-0"> <div className="min-w-0">
<p <p
className="truncate text-sm font-medium text-strong" className="truncate text-sm font-medium text-strong"
title={repositoryDisplayName(repository)} title={displayName}
> >
{repositoryDisplayName(repository)} {displayName}
</p> </p>
<p <p
className="truncate text-xs text-muted" className="truncate text-xs text-muted"
title={`${repository.owner}/${repository.repo}`} title={subtitle}
> >
{repository.owner}/{repository.repo} {subtitle}
</p> </p>
</div> </div>
{canWrite ? ( {canWrite ? (
@ -271,7 +284,7 @@ export function BoardForgejoRepositoryLinks({
setUnlinkError(null); setUnlinkError(null);
setUnlinkTarget(link); setUnlinkTarget(link);
}} }}
aria-label={`Unlink ${repositoryDisplayName(repository)}`} aria-label={`Unlink ${displayName}`}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
@ -303,6 +316,13 @@ export function BoardForgejoRepositoryLinks({
/> />
</div> </div>
{availableLoadError ? (
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[color:var(--danger)]/35 bg-[color:var(--danger-soft)] px-3 py-2 text-sm text-[color:var(--danger)]">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{availableLoadError}</span>
</div>
) : null}
{availableRepos.length === 0 ? ( {availableRepos.length === 0 ? (
<div className="rounded-lg border border-dashed border-[color:var(--border)] bg-[color:var(--surface-muted)] px-4 py-5 text-center"> <div className="rounded-lg border border-dashed border-[color:var(--border)] bg-[color:var(--surface-muted)] px-4 py-5 text-center">
<p className="text-sm font-medium text-strong"> <p className="text-sm font-medium text-strong">
@ -377,7 +397,7 @@ export function BoardForgejoRepositoryLinks({
title="Unlink Git Project repository" title="Unlink Git Project repository"
description={ description={
unlinkTarget 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?" : "Remove this repository from the board?"
} }
onConfirm={handleUnlinkRepo} onConfirm={handleUnlinkRepo}