g1t/apps/web/app/routes/repo/issue-new.tsx

86 lines3,182 bytesCodeBlame
1import { Form, redirect } from "react-router";
2
3import type { Route } from "./+types/issue-new";
4import { Button, ErrorText, Field, Input, Textarea } from "../../components/ui";
5import { Label } from "../../components/work";
6import { work } from "../../lib/services.server";
7import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server";
8
9export function meta({ params }: Route.MetaArgs) {
10 return [{ title: `New issue · ${params.owner}/${params.repo} · g1t` }];
11}
12
13export async function loader({ request, params, context }: Route.LoaderArgs) {
14 const user = requireUser(context, request);
15 const path = { namespace: params.owner, name: params.repo };
16 return { labels: unwrap(await work.listLabels(path, user)) };
17}
18
19export async function action({ request, params, context }: Route.ActionArgs) {
20 assertSameOrigin(request);
21 const user = requireUser(context, request);
22 const form = await request.formData();
23 const result = await work.openIssue(
24 user,
25 { namespace: params.owner, name: params.repo },
26 {
27 title: String(form.get("title") ?? ""),
28 body: String(form.get("body") ?? ""),
29 labels: [
30 ...form.getAll("label").map(String),
31 ...String(form.get("labels") ?? "").split(","),
32 ],
33 checks: String(form.get("checks") ?? "").split("\n"),
34 },
35 );
36 if (!result.ok) return { error: result.error.message };
37 throw redirect(`/${params.owner}/${params.repo}/issues/${result.value.number}`);
38}
39
40export default function NewIssue({ loaderData, actionData }: Route.ComponentProps) {
41 return (
42 <Form method="post" className="max-w-2xl space-y-4">
43 <Field label="Title">
44 <Input
45 name="title"
46 required
47 autoFocus
48 maxLength={200}
49 placeholder="Parser drops the last line of a file without a trailing newline"
50 />
51 </Field>
52 <Field
53 label="Description"
54 hint="What is wrong or wanted, and anything needed to act on it. An agent given this issue works from this text."
55 >
56 <Textarea name="body" rows={8} />
57 </Field>
58 <fieldset>
59 <legend className="mb-1.5 text-sm font-medium text-muted">Labels</legend>
60 <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
61 {loaderData.labels.map((name) => (
62 <label key={name} className="flex cursor-pointer items-center gap-1.5">
63 <input type="checkbox" name="label" value={name} className="accent-accent" />
64 <Label name={name} />
65 </label>
66 ))}
67 </div>
68 <div className="mt-2">
69 <Input
70 name="labels"
71 aria-label="Other labels"
72 placeholder="Others, separated by commas: performance, good first issue"
73 />
74 </div>
75 </fieldset>
76 <Field
77 label="Acceptance checks (optional)"
78 hint="One command per line. A pull request for this issue should make them all pass."
79 >
80 <Textarea name="checks" rows={3} placeholder="cargo test" />
81 </Field>
82 <ErrorText>{actionData?.error}</ErrorText>
83 <Button type="submit">Open issue</Button>
84 </Form>
85 );
86}