This commit is contained in:
null 2026-05-25 14:48:24 -05:00
parent 63fa79b95e
commit e54a29230d
4 changed files with 71 additions and 9 deletions

View File

@ -40,6 +40,7 @@ import { getApiBaseUrl } from "@/lib/api-base";
import { import {
getForgejoRepositories, getForgejoRepositories,
deleteForgejoRepository, deleteForgejoRepository,
refreshRepositoryRecentIssues,
syncRepository, syncRepository,
validateRepository, validateRepository,
type ForgejoRepository, type ForgejoRepository,
@ -531,10 +532,10 @@ export default function ForgejoRepositoriesPage() {
const handleSync = async (repository: ForgejoRepository) => { const handleSync = async (repository: ForgejoRepository) => {
try { try {
const result = await syncRepository(repository.id); const result = await refreshRepositoryRecentIssues(repository.id);
setNotice({ setNotice({
tone: "success", tone: "success",
message: `${repositoryName(repository)} synced: ${result.created} created, ${result.updated} updated, ${result.open} open, ${result.closed} closed.`, message: `${repositoryName(repository)} refreshed: ${result.created} created, ${result.updated} updated, ${result.open} open, ${result.closed} closed.`,
}); });
// Refetch to update last_sync_at // Refetch to update last_sync_at
const data = await getForgejoRepositories(); const data = await getForgejoRepositories();

View File

@ -272,7 +272,7 @@ textarea::placeholder {
animation: progress-shimmer 1.8s linear infinite; animation: progress-shimmer 1.8s linear infinite;
} }
.animate-ticker { .animate-ticker {
animation: ticker-scroll 45s linear infinite; animation: ticker-scroll 70s linear infinite;
} }
.ticker-fade-mask { .ticker-fade-mask {
-webkit-mask-image: linear-gradient(to right, transparent 0px, black 48px, black calc(100% - 48px), transparent 100%); -webkit-mask-image: linear-gradient(to right, transparent 0px, black 48px, black calc(100% - 48px), transparent 100%);

View File

@ -22,6 +22,7 @@ import {
Archive, Archive,
CheckCircle2, CheckCircle2,
CircleDot, CircleDot,
ExternalLink,
Eye, Eye,
GitBranch, GitBranch,
GitCommitHorizontal, GitCommitHorizontal,
@ -51,6 +52,25 @@ const formatConnectionUrl = (value?: string | null) => {
return value.replace(/^https?:\/\//i, "").replace(/\/$/, ""); return value.replace(/^https?:\/\//i, "").replace(/\/$/, "");
}; };
const buildRepositoryUrl = (repo: ForgejoRepository) => {
const baseUrl = repo.connection?.base_url;
if (!baseUrl) return null;
const normalizedBaseUrl = /^https?:\/\//i.test(baseUrl)
? baseUrl
: `https://${baseUrl}`;
try {
const base = normalizedBaseUrl.endsWith("/")
? normalizedBaseUrl
: `${normalizedBaseUrl}/`;
return new URL(
`${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}`,
base,
).toString();
} catch {
return null;
}
};
const repositoryTone = (repo: ForgejoRepository) => { const repositoryTone = (repo: ForgejoRepository) => {
if (repo.last_sync_error) return "danger"; if (repo.last_sync_error) return "danger";
if (!repo.active || repo.is_archived) return "muted"; if (!repo.active || repo.is_archived) return "muted";
@ -300,14 +320,33 @@ const columns = (
accessorKey: "connection", accessorKey: "connection",
header: "Connection", header: "Connection",
cell: ({ row }) => { cell: ({ row }) => {
const connection = row.original.connection; const repo = row.original;
const repositoryUrl = buildRepositoryUrl(repo);
const label = formatConnectionUrl(repo.connection?.base_url);
if (!repositoryUrl) {
return ( return (
<div className="min-w-[180px]"> <div className="min-w-[180px]">
<span className="mt-1 inline-flex max-w-[240px] items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-2 py-0.5 font-mono text-[11px] text-muted"> <span className="mt-1 inline-flex max-w-[240px] items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-2 py-0.5 font-mono text-[11px] text-muted">
{formatConnectionUrl(connection?.base_url)} {label}
</span> </span>
</div> </div>
); );
}
return (
<div className="min-w-[180px]">
<a
href={repositoryUrl}
target="_blank"
rel="noreferrer"
className="mt-1 inline-flex max-w-[240px] items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-2 py-0.5 font-mono text-[11px] text-muted transition hover:border-[color:var(--accent)] hover:text-[color:var(--accent)]"
title={`Open ${repo.owner}/${repo.repo}`}
>
<span className="truncate">{label}</span>
<ExternalLink className="h-3 w-3 shrink-0" />
</a>
</div>
);
}, },
}, },
{ {
@ -503,7 +542,7 @@ function ActionsCell({
onClick={handleSync} onClick={handleSync}
disabled={isSyncLoading} disabled={isSyncLoading}
className="h-7 w-7 rounded-lg p-0 text-muted hover:bg-[color:var(--surface-muted)] hover:text-strong" className="h-7 w-7 rounded-lg p-0 text-muted hover:bg-[color:var(--surface-muted)] hover:text-strong"
aria-label={`Sync issues for ${repository.display_name || `${repository.owner}/${repository.repo}`}`} aria-label={`Refresh recent issues for ${repository.display_name || `${repository.owner}/${repository.repo}`}`}
> >
{isSyncLoading ? ( {isSyncLoading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
@ -514,7 +553,7 @@ function ActionsCell({
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Sync issues</TooltipContent> <TooltipContent>Refresh recent issues</TooltipContent>
</Tooltip> </Tooltip>
)} )}
{onValidate && ( {onValidate && (

View File

@ -258,6 +258,28 @@ export async function syncRepository(repositoryId: string): Promise<{
}); });
} }
export async function refreshRepositoryRecentIssues(
repositoryId: string,
days = 7,
): Promise<{
created: number;
updated: number;
open: number;
closed: number;
total: number;
}> {
const params = new URLSearchParams({ days: String(days) });
return fetchJson<{
created: number;
updated: number;
open: number;
closed: number;
total: number;
}>(`/api/v1/forgejo/repositories/${repositoryId}/sync/recent?${params}`, {
method: "POST",
});
}
export interface ForgejoValidationStatus { export interface ForgejoValidationStatus {
ok: boolean; ok: boolean;
status: string; status: string;