feat: #24
This commit is contained in:
parent
9ff87b7e20
commit
31e3b07d24
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue