g1t/apps/web/app/routes/repo/pulls.tsx

87 lines3,458 bytesCodeBlame
1import { Bot, GitBranch, Plus } from "lucide-react";
2import { Link } from "react-router";
3
4import type { Route } from "./+types/pulls";
5import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui";
6import { PullIcon, StateTabs } from "../../components/work";
7import { work } from "../../lib/services.server";
8import { getViewer, unwrap } from "../../lib/session.server";
9
10export function meta({ params }: Route.MetaArgs) {
11 return [{ title: `Pull requests · ${params.owner}/${params.repo} · g1t` }];
12}
13
14export async function loader({ params, context, request }: Route.LoaderArgs) {
15 const path = { namespace: params.owner, name: params.repo };
16 const state =
17 new URL(request.url).searchParams.get("state") === "closed" ? "closed" : "open";
18 return {
19 pulls: unwrap(await work.listPulls(path, getViewer(context), state)),
20 state,
21 } as const;
22}
23
24export default function Pulls({ loaderData, params }: Route.ComponentProps) {
25 const { pulls, state } = loaderData;
26 const base = `/${params.owner}/${params.repo}`;
27 return (
28 <div>
29 <StateTabs
30 to={`${base}/pulls`}
31 state={state}
32 action={
33 <ButtonLink to={`${base}/pulls/new`}>
34 <Plus size={15} />
35 New pull request
36 </ButtonLink>
37 }
38 />
39 <div className="mt-4">
40 {pulls.length === 0 ? (
41 <EmptyState
42 title={state === "open" ? "No open pull requests" : "No closed pull requests"}
43 >
44 A pull request proposes a change. Assign agents to an issue and
45 each opens one in its own fork, or push a branch and open one
46 yourself.
47 </EmptyState>
48 ) : (
49 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line">
50 {pulls.map((pull) => (
51 <li key={pull.id}>
52 <Link
53 to={`${base}/pull/${pull.number}`}
54 className="flex items-start gap-3 px-4 py-3 transition-colors hover:bg-surface"
55 >
56 <span className="mt-0.5">
57 <PullIcon status={pull.status} />
58 </span>
59 <span className="min-w-0 grow">
60 <span className="flex items-center gap-2">
61 <span className="truncate font-medium">{pull.title}</span>
62 {pull.status === "draft" && (
63 <span className="shrink-0 rounded-full border border-line px-1.5 py-px text-[0.6875rem] text-faint">
64 draft
65 </span>
66 )}
67 </span>
68 <span className="mt-0.5 block text-xs text-faint">
69 #{pull.number} opened <TimeAgo at={pull.createdAt} /> by{" "}
70 {pull.author.username}
71 {pull.issue != null && <> · for #{pull.issue}</>}
72 {pull.supersededBy != null && <> · superseded by #{pull.supersededBy}</>}
73 </span>
74 </span>
75 <span className="mt-0.5 flex shrink-0 items-center gap-1 font-mono text-xs text-muted">
76 {pull.branch ? <GitBranch size={13} /> : <Bot size={13} />}
77 {pull.branch ?? pull.agent}
78 </span>
79 </Link>
80 </li>
81 ))}
82 </ul>
83 )}
84 </div>
85 </div>
86 );
87}