"use client"; import { useCallback, useMemo, useState } from "react"; import { type ColumnDef, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { CircleDot, ExternalLink, XCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { DataTable } from "@/components/tables/DataTable"; import { CloseForgejoIssueDialog } from "@/components/git/CloseForgejoIssueDialog"; import type { ForgejoIssue, ForgejoIssueLabel } from "@/lib/api-forgejo"; /** Normalize a Forgejo label color to a valid 6-char hex string or null. */ function normalizeLabelColor(raw: string | null | undefined): string | null { if (!raw) return null; const hex = raw.replace(/^#+/, ""); return /^[0-9a-fA-F]{3,6}$/.test(hex) ? `#${hex.padEnd(6, "0")}` : null; } /** Return white or dark text based on WCAG relative luminance of a hex background. */ function labelTextColor(hex: string): string { const h = hex.replace("#", ""); const r = parseInt(h.slice(0, 2), 16) / 255; const g = parseInt(h.slice(2, 4), 16) / 255; const b = parseInt(h.slice(4, 6), 16) / 255; const lin = (c: number) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b); return L > 0.179 ? "#1a1a1a" : "#ffffff"; } function LabelChip({ label }: { label: ForgejoIssueLabel }) { const bg = normalizeLabelColor(label.color); return ( {label.name} ); } export type ForgejoIssuesTableProps = { issues: ForgejoIssue[]; isLoading?: boolean; onRefresh: () => void; }; export function ForgejoIssuesTable({ issues, isLoading = false, onRefresh, }: ForgejoIssuesTableProps) { const [closeIssueDialogOpen, setCloseIssueDialogOpen] = useState(false); const [issueToClose, setIssueToClose] = useState(null); const handleCloseClick = useCallback((issue: ForgejoIssue) => { setIssueToClose(issue); setCloseIssueDialogOpen(true); }, []); const handleCloseSuccess = () => { onRefresh(); }; const columns: ColumnDef[] = useMemo( () => [ { accessorKey: "forgejo_issue_number", header: "#", cell: ({ row }) => ( #{row.original.forgejo_issue_number} ), }, { accessorKey: "title", header: "Title", cell: ({ row }) => (
{row.original.title}
), }, { accessorKey: "body_preview", header: "Description", cell: ({ row }) => { const body = row.original.body_preview; if (!body) return null; const truncated = body.length > 120 ? body.slice(0, 120) + "…" : body; return (
{truncated}
); }, }, { accessorKey: "state", header: "State", cell: ({ row }) => { const state = row.original.state; return ( {state} ); }, }, { accessorKey: "labels", header: "Labels", cell: ({ row }) => { const labels = row.original.labels; if (!labels || labels.length === 0) return null; const visible = labels.slice(0, 3); const overflow = labels.length - visible.length; return (
{visible.map((label, i) => ( ))} {overflow > 0 && ( +{overflow} )}
); }, }, { accessorKey: "author", header: "Author", cell: ({ row }) => ( {row.original.author || "Unknown"} ), }, { accessorKey: "forgejo_updated_at", header: "Updated", cell: ({ row }) => { try { return ( {new Date(row.original.forgejo_updated_at).toLocaleDateString()} ); } catch { return ( {row.original.forgejo_updated_at} ); } }, }, { id: "actions", header: "Actions", cell: ({ row }) => { const issue = row.original; if (issue.state !== "open" || issue.is_pull_request) return null; return ( ); }, }, ], [handleCloseClick], ); const table = useReactTable({ data: issues, columns, getCoreRowModel: getCoreRowModel(), }); return ( <>
, title: "No Git Project issues found", description: "Sync a repository to pull issues into Pipeline, or adjust your filters.", }} />
); }