ui upgrade
This commit is contained in:
parent
e6c2989c64
commit
c71b96421f
|
|
@ -2,12 +2,22 @@
|
|||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { type ReactNode, useState, useEffect, useMemo } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
CircleDot,
|
||||
GitBranch,
|
||||
Loader2,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Timer,
|
||||
} from "lucide-react";
|
||||
|
||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
type getMyMembershipApiV1OrganizationsMeMemberGetResponse,
|
||||
|
|
@ -56,6 +66,74 @@ const isRecentlyClosedIssue = (
|
|||
return closedAt.getTime() >= cutoffMs;
|
||||
};
|
||||
|
||||
type StatTone = "blue" | "green" | "amber" | "rose" | "violet";
|
||||
|
||||
const statToneClasses: Record<
|
||||
StatTone,
|
||||
{ card: string; icon: string; glow: string }
|
||||
> = {
|
||||
blue: {
|
||||
card: "border-[color:rgba(96,165,250,0.34)] bg-[linear-gradient(145deg,rgba(96,165,250,0.16),var(--surface)_46%)]",
|
||||
icon: "border-[color:rgba(96,165,250,0.3)] bg-[color:rgba(96,165,250,0.14)] text-[color:var(--accent-strong)]",
|
||||
glow: "bg-[color:rgba(96,165,250,0.32)]",
|
||||
},
|
||||
green: {
|
||||
card: "border-[color:rgba(52,211,153,0.34)] bg-[linear-gradient(145deg,rgba(52,211,153,0.15),var(--surface)_46%)]",
|
||||
icon: "border-[color:rgba(52,211,153,0.3)] bg-[color:rgba(52,211,153,0.14)] text-[color:var(--success)]",
|
||||
glow: "bg-[color:rgba(52,211,153,0.28)]",
|
||||
},
|
||||
amber: {
|
||||
card: "border-[color:rgba(251,191,36,0.36)] bg-[linear-gradient(145deg,rgba(251,191,36,0.14),var(--surface)_46%)]",
|
||||
icon: "border-[color:rgba(251,191,36,0.32)] bg-[color:rgba(251,191,36,0.13)] text-[color:var(--warning)]",
|
||||
glow: "bg-[color:rgba(251,191,36,0.28)]",
|
||||
},
|
||||
rose: {
|
||||
card: "border-[color:rgba(248,113,113,0.34)] bg-[linear-gradient(145deg,rgba(248,113,113,0.14),var(--surface)_46%)]",
|
||||
icon: "border-[color:rgba(248,113,113,0.3)] bg-[color:rgba(248,113,113,0.13)] text-[color:var(--danger)]",
|
||||
glow: "bg-[color:rgba(248,113,113,0.26)]",
|
||||
},
|
||||
violet: {
|
||||
card: "border-[color:rgba(168,85,247,0.32)] bg-[linear-gradient(145deg,rgba(168,85,247,0.14),var(--surface)_46%)]",
|
||||
icon: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.13)] text-[color:rgb(196,181,253)]",
|
||||
glow: "bg-[color:rgba(168,85,247,0.25)]",
|
||||
},
|
||||
};
|
||||
|
||||
function IssueStatCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
caption,
|
||||
tone,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
caption: string;
|
||||
tone: StatTone;
|
||||
}) {
|
||||
const colors = statToneClasses[tone];
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-xl border p-4 shadow-lush ${colors.card}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none absolute inset-x-4 top-0 h-px ${colors.glow}`}
|
||||
/>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
{label}
|
||||
</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-strong">{value}</p>
|
||||
</div>
|
||||
<div className={`rounded-lg border p-2 ${colors.icon}`}>{icon}</div>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted">{caption}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GitIssuesPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const initialStaleOnly = searchParams.get("stale") === "1";
|
||||
|
|
@ -208,6 +286,38 @@ export default function GitIssuesPage() {
|
|||
const isClientFiltered = staleOnly || recentClosedOnly;
|
||||
const visibleTotal = isClientFiltered ? visibleIssues.length : total;
|
||||
const totalPages = isClientFiltered ? 1 : Math.ceil(total / limit);
|
||||
const openIssueCount = useMemo(
|
||||
() => visibleIssues.filter((issue) => issue.state === "open").length,
|
||||
[visibleIssues],
|
||||
);
|
||||
const closedIssueCount = useMemo(
|
||||
() => visibleIssues.filter((issue) => issue.state === "closed").length,
|
||||
[visibleIssues],
|
||||
);
|
||||
const staleIssueCount = useMemo(
|
||||
() =>
|
||||
visibleIssues.filter((issue) => isStaleOpenIssue(issue, staleCutoffMs))
|
||||
.length,
|
||||
[staleCutoffMs, visibleIssues],
|
||||
);
|
||||
const pullRequestCount = useMemo(
|
||||
() => issues.filter((issue) => issue.is_pull_request).length,
|
||||
[issues],
|
||||
);
|
||||
const activeRepositoryName = useMemo(() => {
|
||||
if (repoFilter === "all") return "All repositories";
|
||||
const repo = repos.find((candidate) => candidate.id === repoFilter);
|
||||
return repo
|
||||
? repo.display_name || `${repo.owner}/${repo.repo}`
|
||||
: "Selected repository";
|
||||
}, [repoFilter, repos]);
|
||||
const activeModeLabel = staleOnly
|
||||
? "Stale review"
|
||||
: recentClosedOnly
|
||||
? "Recently closed"
|
||||
: stateFilter === "all"
|
||||
? "All issue states"
|
||||
: `${stateFilter[0]?.toUpperCase() ?? ""}${stateFilter.slice(1)} issues`;
|
||||
|
||||
return (
|
||||
<DashboardPageLayout
|
||||
|
|
@ -219,8 +329,112 @@ export default function GitIssuesPage() {
|
|||
title="Git Project Issues"
|
||||
description={`${visibleTotal} issue${visibleTotal === 1 ? "" : "s"} from repositories tracked by Pipeline.`}
|
||||
stickyHeader
|
||||
headerActions={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoadingIssues}
|
||||
>
|
||||
{isLoadingIssues ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
Refresh
|
||||
</Button>
|
||||
{canCreateIssues && repos.length > 0 ? (
|
||||
<Button size="sm" onClick={() => setCreateDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Issue
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="space-y-5">
|
||||
<section className="relative overflow-hidden rounded-xl border border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.09)_34%,rgba(251,191,36,0.08)_68%,var(--surface)_100%)] p-4 shadow-lush md:p-5">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(52,211,153,0.85),rgba(251,191,36,0))]" />
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<Badge
|
||||
variant={staleIssueCount > 0 ? "warning" : "success"}
|
||||
className="w-fit shadow-[0_0_24px_rgba(96,165,250,0.16)]"
|
||||
>
|
||||
{staleIssueCount > 0 ? `${staleIssueCount} stale` : "Current"}
|
||||
</Badge>
|
||||
<h2 className="mt-3 font-heading text-2xl font-semibold text-strong">
|
||||
Issue Operations
|
||||
</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
||||
{activeModeLabel} across {activeRepositoryName}. Pull requests
|
||||
are hidden from the issue workflow unless a filter explicitly
|
||||
requests upstream totals.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={staleOnly ? "primary" : "outline"}
|
||||
onClick={() => {
|
||||
setStaleOnly((value) => !value);
|
||||
setRecentClosedOnly(false);
|
||||
setStateFilter("open");
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<Timer className="h-4 w-4" />
|
||||
Stale
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={recentClosedOnly ? "primary" : "outline"}
|
||||
onClick={() => {
|
||||
setRecentClosedOnly((value) => !value);
|
||||
setStaleOnly(false);
|
||||
setStateFilter("closed");
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Recent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<IssueStatCard
|
||||
icon={<CircleDot className="h-4 w-4" />}
|
||||
label="Open"
|
||||
value={String(openIssueCount)}
|
||||
caption="Visible issues still active."
|
||||
tone="green"
|
||||
/>
|
||||
<IssueStatCard
|
||||
icon={<CheckCircle2 className="h-4 w-4" />}
|
||||
label="Closed"
|
||||
value={String(closedIssueCount)}
|
||||
caption={`Closed in this result set.`}
|
||||
tone="violet"
|
||||
/>
|
||||
<IssueStatCard
|
||||
icon={<Timer className="h-4 w-4" />}
|
||||
label="Stale"
|
||||
value={String(staleIssueCount)}
|
||||
caption={`${STALE_ISSUE_DAYS}+ days without updates.`}
|
||||
tone={staleIssueCount > 0 ? "amber" : "blue"}
|
||||
/>
|
||||
<IssueStatCard
|
||||
icon={<GitBranch className="h-4 w-4" />}
|
||||
label="Scope"
|
||||
value={repoFilter === "all" ? String(repos.length) : "1"}
|
||||
caption={`${pullRequestCount} pull requests excluded.`}
|
||||
tone="blue"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ForgejoIssueFilters
|
||||
stateFilter={stateFilter}
|
||||
onStateChange={(v) => {
|
||||
|
|
@ -241,96 +455,88 @@ export default function GitIssuesPage() {
|
|||
}}
|
||||
repos={repos}
|
||||
/>
|
||||
{canCreateIssues && repos.length > 0 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
>
|
||||
Create Issue
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{staleOnly ? (
|
||||
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--warning)]/35 bg-[color:rgba(251,191,36,0.12)] p-3 text-sm text-[color:var(--warning)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>
|
||||
Showing open issues not updated in {STALE_ISSUE_DAYS}+ days.
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full text-[color:var(--warning)] hover:bg-[color:rgba(251,191,36,0.14)] sm:w-auto"
|
||||
onClick={() => {
|
||||
setStaleOnly(false);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
Show All Open Issues
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{recentClosedOnly ? (
|
||||
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--success)]/35 bg-[color:rgba(52,211,153,0.12)] p-3 text-sm text-[color:var(--success)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>
|
||||
Showing issues closed in the last {RECENT_CLOSED_DAYS} days.
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full text-[color:var(--success)] hover:bg-[color:rgba(52,211,153,0.14)] sm:w-auto"
|
||||
onClick={() => {
|
||||
setRecentClosedOnly(false);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
Show All Closed Issues
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="mb-4 flex items-start gap-3 rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-sm text-[color:var(--danger)]">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ForgejoIssuesTable
|
||||
issues={visibleIssues}
|
||||
repositories={repos}
|
||||
isLoading={isLoadingIssues}
|
||||
canClose={canCloseIssues}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex flex-col gap-3 text-sm text-muted sm:flex-row sm:items-center sm:justify-between">
|
||||
<span className="break-words">
|
||||
Page {page} of {totalPages} ({visibleTotal} total)
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
{staleOnly ? (
|
||||
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--warning)]/35 bg-[color:rgba(251,191,36,0.12)] p-3 text-sm text-[color:var(--warning)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>
|
||||
Showing open issues not updated in {STALE_ISSUE_DAYS}+ days.
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
className="w-full text-[color:var(--warning)] hover:bg-[color:rgba(251,191,36,0.14)] sm:w-auto"
|
||||
onClick={() => {
|
||||
setStaleOnly(false);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
Next
|
||||
Show All Open Issues
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{recentClosedOnly ? (
|
||||
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--success)]/35 bg-[color:rgba(52,211,153,0.12)] p-3 text-sm text-[color:var(--success)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>
|
||||
Showing issues closed in the last {RECENT_CLOSED_DAYS} days.
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full text-[color:var(--success)] hover:bg-[color:rgba(52,211,153,0.14)] sm:w-auto"
|
||||
onClick={() => {
|
||||
setRecentClosedOnly(false);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
Show All Closed Issues
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="mb-4 flex items-start gap-3 rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-sm text-[color:var(--danger)]">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ForgejoIssuesTable
|
||||
issues={visibleIssues}
|
||||
repositories={repos}
|
||||
isLoading={isLoadingIssues}
|
||||
canClose={canCloseIssues}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex flex-col gap-3 text-sm text-muted sm:flex-row sm:items-center sm:justify-between">
|
||||
<span className="break-words">
|
||||
Page {page} of {totalPages} ({visibleTotal} total)
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CreateForgejoIssueDialog
|
||||
repositories={repos}
|
||||
open={createDialogOpen}
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ textarea::placeholder {
|
|||
animation: progress-shimmer 1.8s linear infinite;
|
||||
}
|
||||
.animate-ticker {
|
||||
animation: ticker-scroll 40s linear infinite;
|
||||
animation: ticker-scroll 90s linear infinite;
|
||||
}
|
||||
.animate-ticker:hover {
|
||||
animation-play-state: paused;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { ExternalLink, UserPlus } from "lucide-react";
|
||||
|
||||
import {
|
||||
useListAgentsApiV1AgentsGet,
|
||||
|
|
@ -109,7 +109,9 @@ export function AssignIssueAgentDialog({
|
|||
setLinkedBoardsLoaded(true);
|
||||
}
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, issue]);
|
||||
|
||||
const agentsQuery = useListAgentsApiV1AgentsGet<
|
||||
|
|
@ -138,13 +140,9 @@ export function AssignIssueAgentDialog({
|
|||
);
|
||||
|
||||
const agents =
|
||||
agentsQuery.data?.status === 200
|
||||
? (agentsQuery.data.data.items ?? [])
|
||||
: [];
|
||||
agentsQuery.data?.status === 200 ? (agentsQuery.data.data.items ?? []) : [];
|
||||
const allBoards =
|
||||
boardsQuery.data?.status === 200
|
||||
? (boardsQuery.data.data.items ?? [])
|
||||
: [];
|
||||
boardsQuery.data?.status === 200 ? (boardsQuery.data.data.items ?? []) : [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
|
|
@ -236,7 +234,7 @@ export function AssignIssueAgentDialog({
|
|||
disabled={disabled}
|
||||
className={`mt-0.5 inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition ${
|
||||
value
|
||||
? "border-emerald-600 bg-emerald-600"
|
||||
? "border-[color:var(--success)] bg-[color:var(--success)]"
|
||||
: "border-[color:var(--border)] bg-[color:var(--surface-muted)]"
|
||||
} ${disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer"}`}
|
||||
>
|
||||
|
|
@ -255,8 +253,12 @@ export function AssignIssueAgentDialog({
|
|||
if (!isSubmitting) onOpenChange(next);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogContent className="relative max-w-lg overflow-hidden border-[color:rgba(168,85,247,0.28)] bg-[linear-gradient(180deg,rgba(168,85,247,0.08),rgba(96,165,250,0.05)_160px,rgba(255,255,255,0)_240px),var(--surface)]">
|
||||
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(168,85,247,0),rgba(168,85,247,0.82),rgba(96,165,250,0.7),rgba(168,85,247,0))]" />
|
||||
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
|
||||
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(168,85,247,0.3)] bg-[color:rgba(168,85,247,0.13)] text-[color:rgb(196,181,253)]">
|
||||
<UserPlus className="h-4 w-4" />
|
||||
</div>
|
||||
<DialogTitle>Assign to agent</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a Pipeline task for{" "}
|
||||
|
|
@ -268,36 +270,41 @@ export function AssignIssueAgentDialog({
|
|||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2">
|
||||
<div className="rounded-xl border border-[color:rgba(168,85,247,0.22)] bg-[color:rgba(168,85,247,0.07)] px-3 py-2">
|
||||
<p className="truncate text-sm font-medium text-strong">
|
||||
{issue.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{noLinkedBoards ? (
|
||||
<div className="rounded-xl border border-[color:rgba(234,179,8,0.4)] bg-[color:rgba(234,179,8,0.08)] p-4 text-sm">
|
||||
<div className="rounded-xl border border-[color:rgba(251,191,36,0.4)] bg-[color:rgba(251,191,36,0.08)] p-4 text-sm">
|
||||
<p className="font-semibold text-[color:var(--warning,#ca8a04)]">
|
||||
Repository not linked to any board
|
||||
</p>
|
||||
<p className="mt-1 text-muted">
|
||||
Before you can assign an agent, link{" "}
|
||||
<span className="font-medium text-strong">{repositoryName}</span>{" "}
|
||||
to the board that should own this task. Open the target board and
|
||||
use its Git Project repositories panel, or manage tracked
|
||||
<span className="font-medium text-strong">
|
||||
{repositoryName}
|
||||
</span>{" "}
|
||||
to the board that should own this task. Open the target board
|
||||
and use its Git Project repositories panel, or manage tracked
|
||||
repositories first.
|
||||
</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<label className="text-sm font-medium text-strong">
|
||||
Link to board <span className="text-[color:var(--danger)]">*</span>
|
||||
Link to board{" "}
|
||||
<span className="text-[color:var(--danger)]">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={linkBoardId}
|
||||
onChange={(e) => setLinkBoardId(e.target.value)}
|
||||
disabled={boardsQuery.isLoading || isLinkingRepository}
|
||||
className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
className="w-full rounded-xl border border-[color:rgba(251,191,36,0.22)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
>
|
||||
<option value="">
|
||||
{boardsQuery.isLoading ? "Loading boards..." : "Select a board..."}
|
||||
{boardsQuery.isLoading
|
||||
? "Loading boards..."
|
||||
: "Select a board..."}
|
||||
</option>
|
||||
{allBoards.map((board) => (
|
||||
<option key={board.id} value={board.id}>
|
||||
|
|
@ -341,7 +348,7 @@ export function AssignIssueAgentDialog({
|
|||
value={boardId}
|
||||
onChange={(e) => setBoardId(e.target.value)}
|
||||
disabled={isSubmitting || boardsLoading}
|
||||
className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
className="w-full rounded-xl border border-[color:rgba(168,85,247,0.2)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
>
|
||||
{boardsLoading ? (
|
||||
<option>Loading boards…</option>
|
||||
|
|
@ -368,7 +375,7 @@ export function AssignIssueAgentDialog({
|
|||
value={agentId}
|
||||
onChange={(e) => setAgentId(e.target.value)}
|
||||
disabled={isSubmitting || !boardId || agentsQuery.isLoading}
|
||||
className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
className="w-full rounded-xl border border-[color:rgba(168,85,247,0.2)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
>
|
||||
<option value="">Select an agent…</option>
|
||||
{agentsQuery.isLoading ? (
|
||||
|
|
@ -390,12 +397,14 @@ export function AssignIssueAgentDialog({
|
|||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-strong">Priority</label>
|
||||
<label className="text-sm font-medium text-strong">
|
||||
Priority
|
||||
</label>
|
||||
<select
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
className="w-full rounded-xl border border-[color:rgba(168,85,247,0.2)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
|
|
@ -406,7 +415,8 @@ export function AssignIssueAgentDialog({
|
|||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-strong">
|
||||
Agent prompt <span className="text-[color:var(--danger)]">*</span>
|
||||
Agent prompt{" "}
|
||||
<span className="text-[color:var(--danger)]">*</span>
|
||||
</label>
|
||||
<Textarea
|
||||
value={instructions}
|
||||
|
|
@ -414,7 +424,7 @@ export function AssignIssueAgentDialog({
|
|||
disabled={isSubmitting}
|
||||
rows={9}
|
||||
placeholder="Prompt the agent with the goal, context, and expected validation…"
|
||||
className="resize-y text-sm"
|
||||
className="resize-y border-[color:rgba(168,85,247,0.18)] bg-[color:var(--surface-muted)] text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted">
|
||||
This prompt is copied into the Pipeline task and sent with the
|
||||
|
|
@ -423,7 +433,11 @@ export function AssignIssueAgentDialog({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{toggleSwitch(startImmediately, setStartImmediately, isSubmitting)}
|
||||
{toggleSwitch(
|
||||
startImmediately,
|
||||
setStartImmediately,
|
||||
isSubmitting,
|
||||
)}
|
||||
<span className="space-y-0.5">
|
||||
<span className="block text-sm font-medium text-strong">
|
||||
Start immediately
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { XCircle } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -52,8 +53,12 @@ export function CloseForgejoIssueDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogContent className="relative max-w-lg overflow-hidden border-[color:rgba(248,113,113,0.32)] bg-[linear-gradient(180deg,rgba(248,113,113,0.09),rgba(255,255,255,0)_190px),var(--surface)]">
|
||||
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(248,113,113,0),rgba(248,113,113,0.85),rgba(248,113,113,0))]" />
|
||||
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
|
||||
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(248,113,113,0.34)] bg-[color:rgba(248,113,113,0.13)] text-[color:var(--danger)]">
|
||||
<XCircle className="h-4 w-4" />
|
||||
</div>
|
||||
<DialogTitle>Close Git Project issue</DialogTitle>
|
||||
<DialogDescription>
|
||||
Confirm closing{" "}
|
||||
|
|
@ -64,7 +69,7 @@ export function CloseForgejoIssueDialog({
|
|||
the local issue cache.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-3">
|
||||
<div className="rounded-xl border border-[color:rgba(248,113,113,0.22)] bg-[color:rgba(248,113,113,0.06)] p-3">
|
||||
<p className="break-words text-sm font-medium text-strong">
|
||||
{issue.title}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { CircleDot, GitBranch } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -90,8 +91,12 @@ export function CreateForgejoIssueDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogContent className="relative max-w-3xl overflow-hidden border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(180deg,rgba(96,165,250,0.08),rgba(255,255,255,0)_220px),var(--surface)]">
|
||||
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(52,211,153,0.75),rgba(96,165,250,0))]" />
|
||||
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
|
||||
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(96,165,250,0.3)] bg-[color:rgba(96,165,250,0.13)] text-[color:var(--accent-strong)]">
|
||||
<CircleDot className="h-4 w-4" />
|
||||
</div>
|
||||
<DialogTitle>Create issue</DialogTitle>
|
||||
<DialogDescription>
|
||||
Open a new issue on the connected Git repository. The body is
|
||||
|
|
@ -119,9 +124,12 @@ export function CreateForgejoIssueDialog({
|
|||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2 text-sm text-muted">
|
||||
Repository:{" "}
|
||||
<span className="font-medium text-strong">{repoLabel}</span>
|
||||
<div className="flex items-center gap-2 rounded-lg border border-[color:rgba(96,165,250,0.22)] bg-[color:rgba(96,165,250,0.08)] px-3 py-2 text-sm text-muted">
|
||||
<GitBranch className="h-4 w-4 text-[color:var(--accent)]" />
|
||||
<span>
|
||||
Repository:{" "}
|
||||
<span className="font-medium text-strong">{repoLabel}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -147,7 +155,7 @@ export function CreateForgejoIssueDialog({
|
|||
onChange={(e) => setBody(e.target.value)}
|
||||
rows={22}
|
||||
disabled={isSubmitting}
|
||||
className="resize-y font-mono text-xs"
|
||||
className="resize-y border-[color:rgba(96,165,250,0.18)] bg-[color:var(--surface-muted)] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -21,7 +22,11 @@ type EditForgejoIssueDialogProps = {
|
|||
repositoryName: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: (updated: { title: string; body: string | null; state: string }) => void;
|
||||
onSuccess: (updated: {
|
||||
title: string;
|
||||
body: string | null;
|
||||
state: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export function EditForgejoIssueDialog({
|
||||
|
|
@ -47,7 +52,8 @@ export function EditForgejoIssueDialog({
|
|||
if (!issue) return null;
|
||||
|
||||
const isDirty =
|
||||
title.trim() !== issue.title || body !== (issue.body ?? issue.body_preview ?? "");
|
||||
title.trim() !== issue.title ||
|
||||
body !== (issue.body ?? issue.body_preview ?? "");
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim()) return;
|
||||
|
|
@ -59,7 +65,11 @@ export function EditForgejoIssueDialog({
|
|||
if (body !== (issue.body ?? issue.body_preview ?? "")) patch.body = body;
|
||||
|
||||
const result = await editForgejoIssue(issue.id, patch);
|
||||
onSuccess({ title: result.title, body: result.body, state: result.state });
|
||||
onSuccess({
|
||||
title: result.title,
|
||||
body: result.body,
|
||||
state: result.state,
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to edit issue");
|
||||
|
|
@ -78,8 +88,12 @@ export function EditForgejoIssueDialog({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogContent className="relative max-w-2xl overflow-hidden border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(180deg,rgba(96,165,250,0.08),rgba(255,255,255,0)_200px),var(--surface)]">
|
||||
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(96,165,250,0))]" />
|
||||
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
|
||||
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(96,165,250,0.3)] bg-[color:rgba(96,165,250,0.13)] text-[color:var(--accent-strong)]">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</div>
|
||||
<DialogTitle>Edit issue</DialogTitle>
|
||||
<DialogDescription>
|
||||
Editing{" "}
|
||||
|
|
@ -92,7 +106,12 @@ export function EditForgejoIssueDialog({
|
|||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="issue-title" className="text-sm font-medium text-strong">Title</label>
|
||||
<label
|
||||
htmlFor="issue-title"
|
||||
className="text-sm font-medium text-strong"
|
||||
>
|
||||
Title
|
||||
</label>
|
||||
<Input
|
||||
id="issue-title"
|
||||
value={title}
|
||||
|
|
@ -103,14 +122,19 @@ export function EditForgejoIssueDialog({
|
|||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="issue-body" className="text-sm font-medium text-strong">Body</label>
|
||||
<label
|
||||
htmlFor="issue-body"
|
||||
className="text-sm font-medium text-strong"
|
||||
>
|
||||
Body
|
||||
</label>
|
||||
<Textarea
|
||||
id="issue-body"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
rows={10}
|
||||
disabled={isSubmitting}
|
||||
className="resize-none font-mono text-sm"
|
||||
className="resize-none border-[color:rgba(96,165,250,0.18)] bg-[color:var(--surface-muted)] font-mono text-sm"
|
||||
placeholder="Issue body (Markdown supported)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
CheckCircle2,
|
||||
CircleDot,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
MessageSquarePlus,
|
||||
|
|
@ -129,6 +132,7 @@ export function ForgejoIssueDetailDialog({
|
|||
const body = detail?.body ?? issue.body ?? issue.body_preview ?? "";
|
||||
const stateVariant = active.state === "open" ? "success" : "default";
|
||||
const showCloseIssue = canClose && active.state === "open";
|
||||
const isOpenIssue = active.state === "open";
|
||||
|
||||
const handleCloseIssueSuccess = () => {
|
||||
if (detail) setDetail({ ...detail, state: "closed" });
|
||||
|
|
@ -180,7 +184,20 @@ export function ForgejoIssueDetailDialog({
|
|||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl border-[color:var(--border-strong)] bg-[color:var(--surface)]/95 backdrop-blur-xl">
|
||||
<DialogContent
|
||||
className={`relative max-w-4xl overflow-hidden backdrop-blur-xl ${
|
||||
isOpenIssue
|
||||
? "border-[color:rgba(52,211,153,0.3)] bg-[linear-gradient(180deg,rgba(52,211,153,0.08),rgba(96,165,250,0.04)_160px,rgba(255,255,255,0)_260px),var(--surface)]"
|
||||
: "border-[color:rgba(168,85,247,0.3)] bg-[linear-gradient(180deg,rgba(168,85,247,0.08),rgba(96,165,250,0.04)_160px,rgba(255,255,255,0)_260px),var(--surface)]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`pointer-events-none absolute inset-x-4 top-0 h-px ${
|
||||
isOpenIssue
|
||||
? "bg-[linear-gradient(90deg,rgba(52,211,153,0),rgba(52,211,153,0.82),rgba(96,165,250,0.66),rgba(52,211,153,0))]"
|
||||
: "bg-[linear-gradient(90deg,rgba(168,85,247,0),rgba(168,85,247,0.82),rgba(96,165,250,0.66),rgba(168,85,247,0))]"
|
||||
}`}
|
||||
/>
|
||||
<DialogHeader className="gap-5 border-b border-[color:var(--border)] pb-5">
|
||||
<div className="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
|
||||
<div className="min-w-0 space-y-2">
|
||||
|
|
@ -281,6 +298,40 @@ export function ForgejoIssueDetailDialog({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)]/70 p-3">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
{isOpenIssue ? (
|
||||
<CircleDot className="h-3.5 w-3.5 text-[color:var(--success)]" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-[color:rgb(196,181,253)]" />
|
||||
)}
|
||||
State
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-semibold text-strong">
|
||||
{active.state}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)]/70 p-3">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
<MessageSquarePlus className="h-3.5 w-3.5 text-[color:var(--accent)]" />
|
||||
Comments
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-semibold text-strong">
|
||||
{comments.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)]/70 p-3">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
<Clock className="h-3.5 w-3.5 text-[color:var(--warning)]" />
|
||||
Updated
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-semibold text-strong">
|
||||
{formatDateTime(active.forgejo_updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mt-5">
|
||||
<TabsList className="max-w-full overflow-x-auto rounded-2xl bg-[color:var(--surface-muted)]/45">
|
||||
<TabsTrigger className="shrink-0 rounded-xl" value="overview">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { Filter, GitBranch, Search } from "lucide-react";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -30,38 +32,47 @@ export function ForgejoIssueFilters({
|
|||
repos,
|
||||
}: ForgejoIssueFiltersProps) {
|
||||
return (
|
||||
<div className="mb-4 grid gap-3 rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-3 shadow-lush sm:grid-cols-[140px_minmax(200px,280px)_minmax(220px,1fr)]">
|
||||
<Select value={stateFilter} onValueChange={onStateChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="State" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="open">Open</SelectItem>
|
||||
<SelectItem value="closed">Closed</SelectItem>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="grid gap-3 rounded-xl border border-[color:var(--border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0)_72%),var(--surface)] p-3 shadow-lush sm:grid-cols-[150px_minmax(220px,300px)_minmax(240px,1fr)]">
|
||||
<div className="relative">
|
||||
<Filter className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted" />
|
||||
<Select value={stateFilter} onValueChange={onStateChange}>
|
||||
<SelectTrigger className="pl-9">
|
||||
<SelectValue placeholder="State" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="open">Open</SelectItem>
|
||||
<SelectItem value="closed">Closed</SelectItem>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Select value={repoFilter} onValueChange={onRepoChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Repository" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All repositories</SelectItem>
|
||||
{repos.map((r) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{r.display_name || `${r.owner}/${r.repo}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<GitBranch className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted" />
|
||||
<Select value={repoFilter} onValueChange={onRepoChange}>
|
||||
<SelectTrigger className="pl-9">
|
||||
<SelectValue placeholder="Repository" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All repositories</SelectItem>
|
||||
{repos.map((r) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{r.display_name || `${r.owner}/${r.repo}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
placeholder="Search Git Project issues…"
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="min-w-0"
|
||||
/>
|
||||
<div className="relative min-w-0">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" />
|
||||
<Input
|
||||
placeholder="Search Git Project issues..."
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="min-w-0 pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
import { useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
CircleDot,
|
||||
ExternalLink,
|
||||
GitBranch,
|
||||
Loader2,
|
||||
Milestone,
|
||||
Pencil,
|
||||
|
|
@ -40,6 +42,41 @@ function labelTextColor(hex: string): string {
|
|||
return L > 0.179 ? "#1a1a1a" : "#ffffff";
|
||||
}
|
||||
|
||||
function issueTone(issue: ForgejoIssue) {
|
||||
if (issue.state === "closed") return "closed";
|
||||
const updatedAt = new Date(issue.forgejo_updated_at || issue.updated_at);
|
||||
if (!Number.isNaN(updatedAt.getTime())) {
|
||||
const daysSinceUpdate =
|
||||
(Date.now() - updatedAt.getTime()) / (24 * 60 * 60 * 1000);
|
||||
if (daysSinceUpdate >= 14) return "stale";
|
||||
}
|
||||
return "open";
|
||||
}
|
||||
|
||||
const issueToneClasses = {
|
||||
open: {
|
||||
rail: "border-l-[color:var(--success)]",
|
||||
row: "bg-[color:rgba(52,211,153,0.018)] hover:bg-[color:rgba(52,211,153,0.07)]",
|
||||
icon: "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.13)] text-[color:var(--success)]",
|
||||
pill: "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.1)] text-[color:var(--success)]",
|
||||
dot: "bg-[color:var(--success)]",
|
||||
},
|
||||
stale: {
|
||||
rail: "border-l-[color:var(--warning)]",
|
||||
row: "bg-[color:rgba(251,191,36,0.022)] hover:bg-[color:rgba(251,191,36,0.08)]",
|
||||
icon: "border-[color:rgba(251,191,36,0.3)] bg-[color:rgba(251,191,36,0.13)] text-[color:var(--warning)]",
|
||||
pill: "border-[color:rgba(251,191,36,0.3)] bg-[color:rgba(251,191,36,0.11)] text-[color:var(--warning)]",
|
||||
dot: "bg-[color:var(--warning)]",
|
||||
},
|
||||
closed: {
|
||||
rail: "border-l-[color:rgb(196,181,253)]",
|
||||
row: "bg-[color:rgba(168,85,247,0.018)] hover:bg-[color:rgba(168,85,247,0.07)]",
|
||||
icon: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.12)] text-[color:rgb(196,181,253)]",
|
||||
pill: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.1)] text-[color:rgb(196,181,253)]",
|
||||
dot: "bg-[color:rgb(196,181,253)]",
|
||||
},
|
||||
} as const;
|
||||
|
||||
function LabelChip({ label }: { label: ForgejoIssueLabel }) {
|
||||
const bg = normalizeLabelColor(label.color);
|
||||
return (
|
||||
|
|
@ -133,7 +170,7 @@ function AssigneeStack({ issue }: { issue: ForgejoIssue }) {
|
|||
{visible.map((login) => (
|
||||
<span
|
||||
key={login}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full border-2 border-[color:var(--surface)] bg-[color:var(--surface-muted)] text-[10px] font-semibold text-strong shadow-sm"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full border-2 border-[color:var(--surface)] bg-[linear-gradient(145deg,rgba(96,165,250,0.22),rgba(52,211,153,0.14)),var(--surface-muted)] text-[10px] font-semibold text-strong shadow-sm"
|
||||
>
|
||||
{getInitials(login)}
|
||||
</span>
|
||||
|
|
@ -154,8 +191,10 @@ function IssueStateIcon({ state }: { state: string }) {
|
|||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center",
|
||||
isOpen ? "text-[color:var(--success)]" : "text-[color:var(--danger)]",
|
||||
"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border",
|
||||
isOpen
|
||||
? "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.13)] text-[color:var(--success)]"
|
||||
: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.12)] text-[color:rgb(196,181,253)]",
|
||||
)}
|
||||
title={isOpen ? "Open issue" : "Closed issue"}
|
||||
>
|
||||
|
|
@ -233,15 +272,15 @@ export function ForgejoIssuesTable({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-hidden rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-lush">
|
||||
<div className="flex flex-col gap-3 border-b border-[color:var(--border)] bg-[color:var(--surface-muted)] px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="overflow-hidden rounded-xl border border-[color:rgba(96,165,250,0.2)] bg-[linear-gradient(180deg,rgba(96,165,250,0.07),rgba(15,23,36,0)_160px),var(--surface)] shadow-lush">
|
||||
<div className="flex flex-col gap-3 border-b border-[color:var(--border)] bg-[linear-gradient(90deg,rgba(96,165,250,0.15),rgba(52,211,153,0.1),rgba(251,191,36,0.08))] px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<span className="inline-flex items-center gap-1.5 font-medium text-strong">
|
||||
<CircleDot className="h-4 w-4 text-[color:var(--success)]" />
|
||||
{issueCounts.open} Open
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 text-muted">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<CheckCircle2 className="h-4 w-4 text-[color:rgb(196,181,253)]" />
|
||||
{issueCounts.closed} Closed
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -254,7 +293,7 @@ export function ForgejoIssuesTable({
|
|||
<LoadingIssuesList />
|
||||
) : issues.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
|
||||
<div className="mb-4 rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-4 text-[color:var(--text-muted)]">
|
||||
<div className="mb-4 rounded-2xl border border-[color:rgba(96,165,250,0.28)] bg-[color:rgba(96,165,250,0.1)] p-4 text-[color:var(--accent-strong)]">
|
||||
<CircleDot className="h-12 w-12" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-strong">
|
||||
|
|
@ -271,6 +310,8 @@ export function ForgejoIssuesTable({
|
|||
const repositoryName =
|
||||
repositoryNameById.get(issue.repository_id) ??
|
||||
issue.repository_id;
|
||||
const tone = issueTone(issue);
|
||||
const toneClass = issueToneClasses[tone];
|
||||
const visibleLabels = issue.labels.slice(0, 4);
|
||||
const hiddenLabelCount =
|
||||
issue.labels.length - visibleLabels.length;
|
||||
|
|
@ -286,7 +327,11 @@ export function ForgejoIssuesTable({
|
|||
return (
|
||||
<article
|
||||
key={issue.id}
|
||||
className="group grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-3 px-4 py-4 transition-colors hover:bg-[color:var(--surface-muted)] sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:gap-x-4"
|
||||
className={cn(
|
||||
"group grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-3 border-l-4 px-4 py-4 transition-colors sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:gap-x-4",
|
||||
toneClass.rail,
|
||||
toneClass.row,
|
||||
)}
|
||||
>
|
||||
<IssueStateIcon state={issue.state} />
|
||||
|
||||
|
|
@ -303,6 +348,21 @@ export function ForgejoIssuesTable({
|
|||
>
|
||||
{issue.title}
|
||||
</button>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide",
|
||||
toneClass.pill,
|
||||
)}
|
||||
>
|
||||
{tone === "stale" ? (
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
) : issue.state === "open" ? (
|
||||
<CircleDot className="h-3 w-3" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
)}
|
||||
{tone === "stale" ? "Stale" : issue.state}
|
||||
</span>
|
||||
{visibleLabels.map((label, i) => (
|
||||
<LabelChip key={`${label.name}-${i}`} label={label} />
|
||||
))}
|
||||
|
|
@ -314,13 +374,20 @@ export function ForgejoIssuesTable({
|
|||
</div>
|
||||
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted">
|
||||
<span className="font-medium text-strong">
|
||||
<span className="inline-flex min-w-0 items-center gap-1 font-medium text-strong">
|
||||
<GitBranch className="h-3.5 w-3.5 shrink-0 text-[color:var(--accent)]" />
|
||||
{repositoryName}
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
<span className="rounded-full border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-2 py-0.5 font-mono">
|
||||
#{issue.forgejo_issue_number}
|
||||
</span>
|
||||
<span title={formatDateTime(updatedAt)}>
|
||||
<span
|
||||
className={cn(
|
||||
"mr-1 inline-block h-1.5 w-1.5 rounded-full",
|
||||
toneClass.dot,
|
||||
)}
|
||||
/>
|
||||
{stateVerb} {formatRelativeTime(updatedAt)}
|
||||
</span>
|
||||
{issue.author ? <span>by {issue.author}</span> : null}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { MessageSquarePlus } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -64,8 +65,12 @@ export function PostForgejoCommentDialog({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogContent className="relative max-w-lg overflow-hidden border-[color:rgba(52,211,153,0.28)] bg-[linear-gradient(180deg,rgba(52,211,153,0.08),rgba(255,255,255,0)_190px),var(--surface)]">
|
||||
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(52,211,153,0),rgba(52,211,153,0.82),rgba(96,165,250,0.65),rgba(52,211,153,0))]" />
|
||||
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
|
||||
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(52,211,153,0.3)] bg-[color:rgba(52,211,153,0.13)] text-[color:var(--success)]">
|
||||
<MessageSquarePlus className="h-4 w-4" />
|
||||
</div>
|
||||
<DialogTitle>Post comment</DialogTitle>
|
||||
<DialogDescription>
|
||||
Comment on{" "}
|
||||
|
|
@ -76,7 +81,7 @@ export function PostForgejoCommentDialog({
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2">
|
||||
<div className="rounded-xl border border-[color:rgba(52,211,153,0.22)] bg-[color:rgba(52,211,153,0.07)] px-3 py-2">
|
||||
<p className="truncate text-sm font-medium text-strong">
|
||||
{issue.title}
|
||||
</p>
|
||||
|
|
@ -89,7 +94,7 @@ export function PostForgejoCommentDialog({
|
|||
onChange={(e) => setBody(e.target.value)}
|
||||
rows={5}
|
||||
disabled={isSubmitting}
|
||||
className="resize-none"
|
||||
className="resize-none border-[color:rgba(52,211,153,0.18)] bg-[color:var(--surface-muted)]"
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ export function AgentActivityTicker() {
|
|||
const display = [...items, ...items];
|
||||
|
||||
return (
|
||||
<div className="border-t border-[color:var(--border)] bg-[color:var(--surface-muted)] overflow-hidden h-7 flex items-center">
|
||||
<span className="shrink-0 px-3 text-[10px] font-semibold uppercase tracking-widest text-[color:var(--text-quiet)] select-none border-r border-[color:var(--border)] h-full flex items-center">
|
||||
<div className="border-t border-[color:var(--border)] bg-[color:var(--surface-muted)] overflow-hidden h-8 flex items-center">
|
||||
<span className="shrink-0 px-3 text-[11px] font-semibold uppercase tracking-widest text-[color:var(--text-quiet)] select-none border-r border-[color:var(--border)] h-full flex items-center">
|
||||
Live
|
||||
</span>
|
||||
<div className="flex-1 overflow-hidden h-full flex items-center">
|
||||
|
|
@ -70,7 +70,7 @@ export function AgentActivityTicker() {
|
|||
{display.map((item, idx) => (
|
||||
<span
|
||||
key={`${item.id}-${idx}`}
|
||||
className="inline-flex items-center gap-1.5 px-4 text-[10px]"
|
||||
className="inline-flex items-center gap-2 px-6 text-[11px]"
|
||||
>
|
||||
<span className="font-semibold text-[color:var(--accent)]">
|
||||
{item.source}
|
||||
|
|
@ -80,7 +80,7 @@ export function AgentActivityTicker() {
|
|||
<span className="text-[color:var(--text-quiet)] tabular-nums ml-1">
|
||||
{fmtRelative(item.created_at)}
|
||||
</span>
|
||||
<span className="mx-3 text-[color:var(--border)] select-none">
|
||||
<span className="mx-5 text-[color:var(--border)] select-none">
|
||||
│
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Reference in New Issue