Skip to content

Commit

web: issues, pull requests, actions and deployments buttons say they are working until the page shows the change; a re-run shows its new log, a refused invitation reloads beside its reason

syntaqxcommitted Parent9c5afb5Browse files
18 files+254−1700/18 viewed
+7−7
33 * nothing proves a change works, and a button that opens a pull request
44 * adding a starter workflow for the stack g1t finds (routes/repo/add-ci.ts).
55 */
6−import { FlaskConical, LoaderCircle, Plus } from "lucide-react";
6+import { FlaskConical, Plus } from "lucide-react";
77 import { useFetcher } from "react-router";
88
9−import { Button, ErrorText } from "./ui";
9+import { ErrorText, SubmitButton } from "./ui";
1010
1111 export function AddCiPrompt({
1212 owner,
2222 compact?: boolean;
2323 }) {
2424 const fetcher = useFetcher<{ error?: string }>();
25− const adding = fetcher.state !== "idle";
25+
2626 return (
2727 <div className={compact ? "flex gap-3 px-4 py-3" : "flex gap-3 rounded-xl border border-dashed border-line-strong bg-surface p-4"}>
2828 <FlaskConical size={16} className="mt-0.5 shrink-0 text-warn" />
3434 </p>
3535 {canAdd && (
3636 <fetcher.Form method="post" action={`/${owner}/${repo}/add-ci`} className="mt-3 flex flex-wrap items-center gap-3">
37− <Button variant="primary" type="submit" disabled={adding}>
38− {adding ? <LoaderCircle size={14} className="animate-spin" /> : <Plus size={14} />}
39− {adding ? "Opening a pull request…" : "Add CI"}
40− </Button>
37+ <SubmitButton variant="primary" fetcher={fetcher} pending="Opening a pull request…">
38+ <Plus size={14} />
39+ Add CI
40+ </SubmitButton>
4141 <span className="text-xs text-faint">
4242 Opens a pull request with a starter workflow for the stack g1t finds. You can change it before merging.
4343 </span>
+13−8
55 * `agents.json` resource route, so every place behaves the same.
66 */
77 import { Bot, CircleSlash, Clock, Coins, Gauge, Loader2, MessageSquare, OctagonX, Square, TriangleAlert } from "lucide-react";
8−import { type ReactNode, useEffect, useMemo, useState } from "react";
8+import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
99 import { Link, useFetcher } from "react-router";
1010 import { useRefreshWhile } from "../lib/refresh";
1111
2323
2424 import { RunAudit } from "./audit";
2525 import { STAGE_LABEL, StageDots } from "./lifecycle";
26−import { Avatar, TimeAgo } from "./ui";
26+import { Avatar, SubmitButton, TimeAgo } from "./ui";
2727 import {
2828 AlertDialog,
2929 AlertDialogAction,
157157 export function MessageRun({ run }: { run: AgentRun }) {
158158 const fetcher = useFetcher<ActionResult>();
159159 const [open, setOpen] = useState(false);
160+ const form = useRef<HTMLFormElement>(null);
161+ // Sent: the box empties, and the notice below says where it went.
162+ useEffect(() => {
163+ if (fetcher.state === "idle" && fetcher.data?.ok) form.current?.reset();
164+ }, [fetcher.state, fetcher.data]);
160165 const live = takesMessages(run.kind) && isActiveRun(run.status);
161166 if (run.number == null) return null;
162167 return (
174179 : "This run does not read messages while it works. Your message waits on the pull request and is given to the agent's next run there."}
175180 </DialogDescription>
176181 </DialogHeader>
177− <fetcher.Form method="post" action={actionUrl(run)} className="space-y-3">
182+ <fetcher.Form ref={form} method="post" action={actionUrl(run)} className="space-y-3">
178183 <input type="hidden" name="intent" value="message" />
179184 <input type="hidden" name="number" value={run.number} />
180185 <textarea
189194 {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
190195 {fetcher.data?.ok && fetcher.data.notice && <p className="text-sm text-accent">{fetcher.data.notice}</p>}
191196 <div className="flex justify-end">
192− <button
193− type="submit"
194− disabled={fetcher.state !== "idle"}
197+ <SubmitButton
198+ fetcher={fetcher}
199+ pending="Sending…"
195200 className="inline-flex items-center gap-2 rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-white disabled:opacity-50"
196201 >
197− {fetcher.state !== "idle" ? "Sending…" : "Send"}
198− </button>
202+ Send
203+ </SubmitButton>
199204 </div>
200205 </fetcher.Form>
201206 </DialogContent>
+12−3
2121 HighlightedLine,
2222 } from "../lib/diff";
2323 import { Markdown } from "./markdown";
24−import { Avatar, Button, EmptyState, Textarea, TimeAgo } from "./ui";
24+import { Avatar, Button, EmptyState, SubmitButton, Textarea, TimeAgo } from "./ui";
2525 import { Checkbox } from "./ui/checkbox";
2626
2727 const ROW_STYLES: Record<DiffLine["kind"], string> = {
134134 open: boolean;
135135 onClose: () => void;
136136 }) {
137+ // The box stays, saying it is posting, until the new comment shows above it.
138+ const count = useRef(comments.length);
139+ useEffect(() => {
140+ if (comments.length > count.current) onClose();
141+ count.current = comments.length;
142+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only a new comment closes it
143+ }, [comments.length]);
137144 return (
138145 <div className="max-w-2xl space-y-2">
139146 {comments.map((comment) => (
140147 <LineComment key={comment.id} comment={comment} />
141148 ))}
142149 {open && (
143− <Form method="post" className="space-y-2 font-sans" onSubmit={onClose}>
150+ <Form method="post" className="space-y-2 font-sans">
144151 <input type="hidden" name="action" value="comment" />
145152 <input type="hidden" name="path" value={path} />
146153 <input type="hidden" name="line" value={line} />
147154 <Textarea name="body" rows={3} required autoFocus placeholder={`Comment on line ${line}`} />
148155 <div className="flex gap-2">
149− <Button type="submit">Comment</Button>
156+ <SubmitButton match={{ action: "comment", path, line: String(line) }} pending="Commenting…">
157+ Comment
158+ </SubmitButton>
150159 <Button variant="quiet" type="button" onClick={onClose}>
151160 Cancel
152161 </Button>
+8−3
2828 import { catchUpPhase, catchUpRun, catchUpTitle, catchUpWhy } from "../lib/catch-up";
2929 import { duration } from "./actions";
3030 import { Elapsed, useRuns } from "./agents";
31−import { Button, CopyLine, ErrorText, TimeAgo } from "./ui";
31+import { Button, CopyLine, ErrorText, SubmitButton, TimeAgo } from "./ui";
3232 import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
3333
3434 // --- Output ---------------------------------------------------------------
408408 <Form method="post" key={status.context}>
409409 <input type="hidden" name="action" value="rerun-workflow" />
410410 <input type="hidden" name="run" value={runIdOf(status) ?? ""} />
411− <Button variant="quiet" type="submit" title={`Re-run the failed jobs of ${status.context}`}>
411+ <SubmitButton
412+ variant="quiet"
413+ match={{ action: "rerun-workflow", run: runIdOf(status) ?? "" }}
414+ pending="Re-running…"
415+ title={`Re-run the failed jobs of ${status.context}`}
416+ >
412417 <RotateCw size={13} />
413418 Re-run failed jobs{failedRuns.length > 1 ? ` of ${status.context}` : ""}
414− </Button>
419+ </SubmitButton>
415420 </Form>
416421 ))}
417422 </div>
+11−10
3333 import { useLiveRefresh } from "./agents";
3434 import { Unavailable } from "./mission";
3535 import { TokenUsagePanel } from "./token-usage";
36−import { Avatar, TimeAgo } from "./ui";
36+import { Avatar, SubmitButton, TimeAgo } from "./ui";
3737 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu";
3838 import type { Loaded } from "../routes/home";
3939
117117 );
118118 }
119119
120−/** A form that acts on a pull request from here, saying so before the page catches up. */
120+/**
121+ * A form that acts on a pull request from here: working until the action
122+ * has answered and the page has reloaded what it changed, then done, or
123+ * the error beside it.
124+ */
121125 function QuickForm({ quick, variant = "quiet" }: { quick: QuickAction; variant?: "quiet" | "accent" }) {
122126 const fetcher = useFetcher<{ error?: string } | null>();
123127 const [sent, setSent] = useState(false);
124− const busy = fetcher.state !== "idle";
125− // Shown as done the moment it is sent, and undone if it fails: the page
126− // reloads behind it, which takes longer than the action.
127− const done = sent && (busy || !fetcher.data?.error);
128+ const done = sent && fetcher.state === "idle" && !fetcher.data?.error;
128129 return (
129130 <fetcher.Form method="post" action={quick.to} onSubmit={() => setSent(true)} className="contents">
130131 {Object.entries(quick.fields).map(([name, value]) => (
131132 <input key={name} type="hidden" name={name} value={value} />
132133 ))}
133− <button
134− type="submit"
135− disabled={busy || done}
134+ <SubmitButton
135+ fetcher={fetcher}
136+ disabled={done}
136137 className={cn(
137138 "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-default",
138139 variant === "accent"
143144 >
144145 {done ? <Check size={14} /> : null}
145146 {done ? quick.done : quick.label}
146− </button>
147+ </SubmitButton>
147148 {fetcher.data?.error && <span className="basis-full text-xs text-danger">{fetcher.data.error}</span>}
148149 </fetcher.Form>
149150 );
+15−6
88 import { Markdown } from "./markdown";
99 import { IssueIcon, PullIcon } from "./work-icons";
1010 import { MentionTextarea } from "./mention-textarea";
11−import { Avatar, Button, TimeAgo } from "./ui";
11+import { Avatar, SubmitButton, TimeAgo } from "./ui";
1212 import { CheckboxOption } from "./ui/checkbox";
1313
1414 export { IssueIcon, PullIcon };
412412 }
413413 />
414414 <div className="flex flex-wrap gap-2">
415− <Button type="submit">Comment</Button>
415+ {/* A plain comment's verdict is none the action knows, so each button tells its own post apart. */}
416+ <SubmitButton name="verdict" value="comment" match={{ action: "comment" }} pending="Commenting…">
417+ Comment
418+ </SubmitButton>
416419 {review?.canJudge && (
417420 <>
418− <Button variant="quiet" type="submit" name="verdict" value="approve">
421+ <SubmitButton variant="quiet" name="verdict" value="approve" match={{ action: "comment" }} pending="Approving…">
419422 <CircleCheck size={14} className="text-accent" />
420423 Approve
421− </Button>
422− <Button variant="quiet" type="submit" name="verdict" value="request_changes">
424+ </SubmitButton>
425+ <SubmitButton
426+ variant="quiet"
427+ name="verdict"
428+ value="request_changes"
429+ match={{ action: "comment" }}
430+ pending="Requesting changes…"
431+ >
423432 <CircleSlash size={14} className="text-danger" />
424433 Request changes
425− </Button>
434+ </SubmitButton>
426435 </>
427436 )}
428437 </div>
+12−10
11 import { AlertTriangle, ChevronRight, Cloud, Download, GitBranch, GitCommitHorizontal, Info, Package, RotateCw, ServerCog, Square, XCircle } from "lucide-react";
22 import { type ReactNode } from "react";
3−import { Form, Link, useLoaderData, useNavigation, useSearchParams } from "react-router";
3+import { Form, Link, useLoaderData, useSearchParams } from "react-router";
44
55 import type { Annotation, Job, StepState } from "@g1t/contracts";
66
77 import type { Route } from "./+types/actions-run";
88 import { page } from "../../lib/meta";
99 import { LogText, Notes, StatusIcon, duration, shortRef, standingWord, useJobLog } from "../../components/actions";
10−import { Button, ErrorText, TimeAgo } from "../../components/ui";
10+import { ErrorText, SubmitButton, TimeAgo, usePending } from "../../components/ui";
1111 import { listArtifacts } from "../../lib/artifacts.server";
1212 import { actions } from "../../lib/services.server";
1313 import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
184184 const { run, jobs, notes } = detail;
185185 const base = `/${params.owner}/${params.repo}`;
186186 const [search] = useSearchParams();
187− const busy = useNavigation().state === "submitting";
187+ // One of the run's buttons is working: the others wait for it.
188+ const busy = usePending();
188189 const live = run.status !== "completed";
189190 useRefreshWhile(live, 2500);
190191
211212 {member && !run.error && (
212213 <Form method="post" className="flex gap-2">
213214 {live ? (
214− <Button type="submit" name="intent" value="cancel" variant="quiet" disabled={busy}>
215+ <SubmitButton name="intent" value="cancel" variant="quiet" disabled={busy} pending="Cancelling…">
215216 <Square size={13} />
216217 Cancel run
217− </Button>
218+ </SubmitButton>
218219 ) : (
219220 <>
220221 {anyFailed && (
221− <Button type="submit" name="intent" value="rerun-failed" variant="quiet" disabled={busy}>
222+ <SubmitButton name="intent" value="rerun-failed" variant="quiet" disabled={busy} pending="Re-running…">
222223 <RotateCw size={13} />
223224 Re-run failed jobs
224− </Button>
225+ </SubmitButton>
225226 )}
226− <Button type="submit" name="intent" value="rerun" variant="quiet" disabled={busy}>
227+ <SubmitButton name="intent" value="rerun" variant="quiet" disabled={busy} pending="Re-running…">
227228 <RotateCw size={13} />
228229 Re-run all jobs
229− </Button>
230+ </SubmitButton>
230231 </>
231232 )}
232233 </Form>
307308 </Link>
308309 ))}
309310 </nav>
310− {selected && <JobView key={selected.id} job={selected} base={base} />}
311+ {/* Run again, a job keeps its id but its log starts afresh. */}
312+ {selected && <JobView key={`${selected.id}:${run.attempt}`} job={selected} base={base} />}
311313 </div>
312314 )}
313315 </div>
+19−10
11 import { AlertTriangle, FileCode2, GitBranch, Play, PlayCircle } from "lucide-react";
2−import { useState } from "react";
3−import { Form, Link, useNavigation, useSearchParams } from "react-router";
2+import { useEffect, useRef, useState } from "react";
3+import { Form, Link, useSearchParams } from "react-router";
44
55 import type { DispatchInput, Workflow, WorkflowRun } from "@g1t/contracts";
66
88 import { page } from "../../lib/meta";
99 import { Notes, StatusIcon, duration, shortRef } from "../../components/actions";
1010 import { AddCiPrompt } from "../../components/add-ci";
11−import { Button, ComputeNote, EmptyState, ErrorText, TimeAgo } from "../../components/ui";
11+import { Button, ComputeNote, EmptyState, ErrorText, SubmitButton, TimeAgo, usePending } from "../../components/ui";
1212 import { CheckboxOption } from "../../components/ui/checkbox";
1313 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
1414 import { computeNoteFor } from "../../lib/compute.server";
178178
179179 function RunWorkflow({ workflow }: { workflow: Workflow }) {
180180 const [open, setOpen] = useState(false);
181− const busy = useNavigation().state === "submitting";
181+ // Open, saying it is starting, until the run shows below or the error does.
182+ const running = usePending({ intent: "dispatch", workflow: workflow.id });
183+ const was = useRef(false);
184+ useEffect(() => {
185+ if (was.current && !running) setOpen(false);
186+ was.current = running;
187+ }, [running]);
182188 const inputs = Object.entries(workflow.dispatch ?? {});
183189 const booleans = inputs.filter(([, spec]) => spec.type === "boolean").map(([name]) => name);
184190 return (
190196 {open && (
191197 <Form
192198 method="post"
193− onSubmit={() => setOpen(false)}
194199 className="absolute right-0 z-20 mt-2 w-80 space-y-3 rounded-xl border border-line bg-surface p-4 shadow-xl"
195200 >
201+ <input type="hidden" name="intent" value="dispatch" />
196202 <input type="hidden" name="workflow" value={workflow.id} />
197203 <input type="hidden" name="booleans" value={booleans.join(",")} />
198204 <label className="block">
207213 {inputs.map(([name, spec]) => (
208214 <InputField key={name} name={name} spec={spec} />
209215 ))}
210− <Button type="submit" disabled={busy}>
216+ <SubmitButton match={{ intent: "dispatch", workflow: workflow.id }} pending="Starting…">
211217 <PlayCircle size={14} />
212218 Run
213− </Button>
219+ </SubmitButton>
214220 </Form>
215221 )}
216222 </div>
218224 }
219225
220226 function WorkflowHeader({ workflow, base, member, manage }: { workflow: Workflow; base: string; member: boolean; manage: boolean }) {
221− const busy = useNavigation().state === "submitting";
222227 return (
223228 <div className="space-y-4">
224229 <div className="flex flex-wrap items-start justify-between gap-3">
239244 <input type="hidden" name="intent" value="toggle" />
240245 <input type="hidden" name="workflow" value={workflow.id} />
241246 <input type="hidden" name="enabled" value={workflow.state === "active" ? "false" : "true"} />
242− <Button type="submit" variant="quiet" disabled={busy}>
247+ <SubmitButton
248+ variant="quiet"
249+ match={{ intent: "toggle", workflow: workflow.id }}
250+ pending={workflow.state === "active" ? "Turning off…" : "Turning on…"}
251+ >
243252 {workflow.state === "active" ? "Turn off" : "Turn on"}
244− </Button>
253+ </SubmitButton>
245254 </Form>}
246255 </div>
247256 )}
+37−12
11 import { ExternalLink, Globe, Rocket, RotateCw, Trash2 } from "lucide-react";
22 import type { ReactNode } from "react";
3−import { Form, Link, useNavigation } from "react-router";
3+import { Form, Link } from "react-router";
44
55 import type { Deployment, FeatureState } from "@g1t/contracts";
66
88
99 import type { Route } from "./+types/deployments";
1010 import { page } from "../../lib/meta";
11−import { Button, ButtonLink, ComputeNote, EmptyState, ErrorText, TimeAgo } from "../../components/ui";
11+import { ButtonLink, ComputeNote, EmptyState, ErrorText, SubmitButton, TimeAgo } from "../../components/ui";
1212 import { computeNoteFor } from "../../lib/compute.server";
1313 import { billing, deployments, projects } from "../../lib/services.server";
1414 import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
7272
7373 export default function RepoDeployments({ loaderData, actionData, params }: Route.ComponentProps) {
7474 const { settings, deployments: builds, live, plan, can } = loaderData;
75− const busy = useNavigation().state === "submitting";
7675 const base = `/${params.owner}/${params.repo}`;
7776 const production = live.find((app) => app.kind === "production");
7877 const previews = live.filter((app) => app.kind === "preview");
132131 </p>
133132 ) : (
134133 <Form method="post" className="mt-4">
135− <Button variant="accent" type="submit" name="intent" value="enable" disabled={busy || !can.manage_integrations} title={whyNot(can, "manage_integrations")}>
134+ <SubmitButton
135+ variant="accent"
136+ name="intent"
137+ value="enable"
138+ pending="Turning on…"
139+ disabled={!can.manage_integrations}
140+ title={whyNot(can, "manage_integrations")}
141+ >
136142 <Rocket size={14} />
137143 Turn on deployments
138− </Button>
144+ </SubmitButton>
139145 </Form>
140146 )}
141147 </section>
148154 app={production}
149155 off={!settings.production}
150156 actions={
151− can.run ? <AppActions branch={null} up={!!production} busy={busy} /> : null
157+ can.run ? <AppActions branch={null} up={!!production} /> : null
152158 }
153159 />
154160 <div className="rounded-xl border border-line bg-surface p-5">
176182 {host(app.url)}
177183 </a>
178184 <span className="ml-auto shrink-0">
179− {can.run && <AppActions branch={app.branch} up busy={busy} compact />}
185+ {can.run && <AppActions branch={app.branch} up compact />}
180186 </span>
181187 </li>
182188 ))}
283289 );
284290 }
285291
286−function AppActions({ branch, up, busy, compact }: { branch: string | null; up: boolean; busy: boolean; compact?: boolean }) {
292+function AppActions({ branch, up, compact }: { branch: string | null; up: boolean; compact?: boolean }) {
293+ // Which app's buttons say they are working: production has no branch.
294+ const app = branch ?? "production";
287295 return (
288296 <Form method="post" className="flex items-center gap-2">
289297 {branch != null && <input type="hidden" name="branch" value={branch} />}
290− <Button variant="quiet" type="submit" name="intent" value="redeploy" disabled={busy} title="Build again from the current head">
298+ <input type="hidden" name="app" value={app} />
299+ <SubmitButton
300+ variant="quiet"
301+ name="intent"
302+ value="redeploy"
303+ match={{ app }}
304+ icon={compact}
305+ pending="Redeploying…"
306+ title="Build again from the current head"
307+ >
291308 <RotateCw size={13} />
292309 {!compact && "Redeploy"}
293− </Button>
310+ </SubmitButton>
294311 {up && (
295− <Button variant="quiet" type="submit" name="intent" value="take-down" disabled={busy} title="Take it down now">
312+ <SubmitButton
313+ variant="quiet"
314+ name="intent"
315+ value="take-down"
316+ match={{ app }}
317+ icon={compact}
318+ pending="Taking down…"
319+ title="Take it down now"
320+ >
296321 <Trash2 size={13} />
297322 {!compact && "Take down"}
298− </Button>
323+ </SubmitButton>
299324 )}
300325 </Form>
301326 );
+15−12
11 import { Box, CircleAlert, MailOpen } from "lucide-react";
2−import { Form, data, redirect, useNavigation } from "react-router";
2+import { Form, data, redirect } from "react-router";
33
44 import { REPO_ROLE_LABELS, REPO_ROLE_SUMMARIES } from "@g1t/contracts";
55
66 import type { Route } from "./+types/invitations";
7−import { Avatar, Button, ButtonLink, ErrorText } from "../../components/ui";
7+import { Avatar, ButtonLink, ErrorText, SubmitButton, usePending } from "../../components/ui";
88 import { page } from "../../lib/meta";
99 import { identity } from "../../lib/services.server";
1010 import { assertSameOrigin, requireUser } from "../../lib/session.server";
3434 const form = await request.formData();
3535 const accept = form.get("intent") === "accept";
3636 const invitation = await invitationTo(user, params.owner, params.repo);
37− if (!invitation) return data({ error: "This invitation is no longer open." }, { status: 404 });
37+ // Refusals are said with a 200, so the page loads again and shows the
38+ // invitation as it now stands beside the reason: after a 4xx answer
39+ // React Router keeps the page's data as it was.
40+ if (!invitation) return { error: "This invitation is no longer open." };
3841 const answered = await identity.respondRepoInvitation(user, invitation.id, accept);
39− if (!answered.ok) return data({ error: answered.error.message }, { status: 422 });
42+ if (!answered.ok) return { error: answered.error.message };
4043 // The role is theirs from the next request on: straight to the repository.
4144 throw redirect(accept ? `/${invitation.repo}` : "/");
4245 }
4548
4649 export default function RepoInvitation({ loaderData, actionData, params }: Route.ComponentProps) {
4750 const { invitation, viewer } = loaderData;
48− const navigation = useNavigation();
49− const answering = navigation.state !== "idle" ? navigation.formData?.get("intent") : null;
51+ // Either answer: the other button waits for it.
52+ const answering = usePending();
5053 const full = `${params.owner}/${params.repo}`;
5154 if (!invitation) {
5255 return (
117120 </p>
118121 <ErrorText>{actionData?.error}</ErrorText>
119122 <div className="flex flex-col gap-3 sm:flex-row">
120− <Button type="submit" name="intent" value="accept" disabled={answering != null}>
121− {answering === "accept" ? "Accepting…" : "Accept invitation"}
122− </Button>
123− <Button type="submit" name="intent" value="decline" variant="quiet" disabled={answering != null}>
124− {answering === "decline" ? "Declining…" : "Decline"}
125− </Button>
123+ <SubmitButton name="intent" value="accept" disabled={answering} pending="Accepting…">
124+ Accept invitation
125+ </SubmitButton>
126+ <SubmitButton name="intent" value="decline" variant="quiet" disabled={answering} pending="Declining…">
127+ Decline
128+ </SubmitButton>
126129 </div>
127130 </Form>
128131 </div>
+7−6
11 import { env } from "cloudflare:workers";
22 import { Download, Sparkles } from "lucide-react";
3−import { Form, Link, redirect, useNavigation, useSearchParams } from "react-router";
3+import { Form, Link, redirect, useSearchParams } from "react-router";
44
55 import { PROVIDERS } from "@g1t/contracts";
66
77 import type { Route } from "./+types/issue-new";
88 import { page } from "../../lib/meta";
9−import { Button, ErrorText, Field, Input, Textarea } from "../../components/ui";
9+import { ErrorText, Field, Input, SubmitButton, Textarea } from "../../components/ui";
1010 import { CheckboxOption } from "../../components/ui/checkbox";
1111 import { Label } from "../../components/work";
1212 import { integrations, work } from "../../lib/services.server";
7878 }
7979
8080 export default function NewIssue({ loaderData, actionData }: Route.ComponentProps) {
81− const busy = useNavigation().state === "submitting";
8281 // "Put an agent on …" in the palette arrives with ?agent=1.
8382 const [params] = useSearchParams();
8483 const refused = actionData && "notStarted" in actionData ? actionData.notStarted : null;
101100 <Input name="reference" required placeholder="TECH-1234" aria-label="Ticket key or address" />
102101 </div>
103102 <CheckboxOption name="assign" label="Put an agent on it" className="items-center" labelClassName="text-muted" />
104− <Button type="submit" variant="quiet" disabled={busy}>
103+ <SubmitButton variant="quiet" match={{ intent: "import" }} pending="Importing…">
105104 Import
106− </Button>
105+ </SubmitButton>
107106 </div>
108107 {actionData && "importError" in actionData && (
109108 <div className="mt-2">
180179 </div>
181180 )}
182181 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
183− <Button type="submit">Open issue</Button>
182+ <SubmitButton name="intent" value="open" pending="Opening…">
183+ Open issue
184+ </SubmitButton>
184185 </Form>
185186 </div>
186187 );
+24−24
11 import { env } from "cloudflare:workers";
22 import { Bot, ExternalLink, GitCommitHorizontal, GitMerge, Play, Sparkles } from "lucide-react";
3−import { Form, Link, redirect, useNavigation } from "react-router";
3+import { Form, Link, redirect } from "react-router";
44
55 import { type Pull, PROVIDERS, workOwner } from "@g1t/contracts";
66
1010 import { AgentStepLine } from "../../components/agents";
1111 import {
1212 Avatar,
13− Button,
1413 CopyLine,
1514 EmptyState,
1615 ComputeNote,
1716 ErrorText,
1817 Input,
18+ SubmitButton,
1919 Textarea,
2020 TimeAgo,
2121 } from "../../components/ui";
235235 const { issue, pulls, comments, viewer, labels, agentsEnabled, members, canManage, can } = loaderData;
236236
237237 // Follow agents at work without a manual reload.
238− const navigation = useNavigation();
239238 const running = pulls.some(
240239 (pull) =>
241240 pull.status === "draft" ||
253252 );
254253 useRefreshWhile(Boolean(running || assigned));
255254
256− const starting = navigation.formData?.get("action") === "run-hosted";
255+ // A refusal to start g1t shows by its button; every other error below the discussion.
256+ const runError = actionData && "action" in actionData && actionData.action === "run-hosted";
257257 const base = `/${params.owner}/${params.repo}`;
258258 const open = issue.state === "open";
259259 const reference = `${params.owner}/${params.repo}#${issue.number}`;
352352 <Form method="post" className="flex flex-wrap justify-end gap-2">
353353 {open ? (
354354 <>
355− <Button variant="quiet" type="submit" name="action" value="close-not-planned">
355+ <SubmitButton variant="quiet" name="action" value="close-not-planned" pending="Closing…">
356356 Close as not planned
357− </Button>
358− <Button variant="quiet" type="submit" name="action" value="close-completed">
357+ </SubmitButton>
358+ <SubmitButton variant="quiet" name="action" value="close-completed" pending="Closing…">
359359 Close issue
360− </Button>
360+ </SubmitButton>
361361 </>
362362 ) : (
363− <Button variant="quiet" type="submit" name="action" value="reopen">
363+ <SubmitButton variant="quiet" name="action" value="reopen" pending="Reopening…">
364364 Reopen issue
365− </Button>
365+ </SubmitButton>
366366 )}
367367 </Form>
368368 )}
369369 </div>
370370 </div>
371371 <div className="mt-2">
372− {!(actionData && "action" in actionData) && <ErrorText>{actionData?.error}</ErrorText>}
372+ {!runError && <ErrorText>{actionData?.error}</ErrorText>}
373373 </div>
374374 </div>
375375
489489 <Form method="post" className="mt-3 space-y-2">
490490 <input type="hidden" name="action" value="run-hosted" />
491491 <div className="*:w-full">
492− <Button variant="accent" type="submit" disabled={starting}>
492+ <SubmitButton variant="accent" match={{ action: "run-hosted" }} pending="Starting a sandbox…">
493493 <Sparkles size={14} />
494− {starting ? "Starting a sandbox…" : "Assign to g1t"}
495− </Button>
494+ Assign to g1t
495+ </SubmitButton>
496496 </div>
497497 <details>
498498 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
511511 agent and fixes. You get it back ready to merge.
512512 </p>
513513 <ComputeNote note={loaderData.computeNote} />
514− {actionData && "action" in actionData && actionData.action === "run-hosted" && (
515− <ErrorText>{actionData.error}</ErrorText>
516− )}
514+ {runError && <ErrorText>{actionData.error}</ErrorText>}
517515 </Form>
518516 )}
519517
547545 ))}
548546 <input type="hidden" name="assignee" value={viewer.username} />
549547 <div className="*:w-full">
550− <Button variant="quiet" type="submit">
548+ <SubmitButton variant="quiet" name="who" value="self" pending="Assigning…">
551549 Assign yourself
552− </Button>
550+ </SubmitButton>
553551 </div>
554552 </Form>
555553 )}
560558 <Form method="post" className="mt-2 space-y-2" key={issue.assignees.join()}>
561559 <input type="hidden" name="action" value="assign" />
562560 <PeoplePicker name="assignee" members={members} chosen={issue.assignees} />
563− <Button variant="quiet" type="submit">
561+ <SubmitButton variant="quiet" name="who" value="picked" pending="Saving…">
564562 Save assignees
565− </Button>
563+ </SubmitButton>
566564 </Form>
567565 </details>
568566 </div>
587585 </p>
588586 <Input name="agent" placeholder="Who is working, e.g. claude-code" maxLength={60} />
589587 <div className="*:w-full">
590− <Button type="submit">Open pull request</Button>
588+ <SubmitButton match={{ action: "open-pull" }} pending="Opening…">
589+ Open pull request
590+ </SubmitButton>
591591 </div>
592592 <p className="text-xs text-muted">
593593 Already pushed a branch?{" "}
631631 ))}
632632 </div>
633633 <Input name="labels" placeholder="New labels, comma separated" />
634− <Button variant="quiet" type="submit">
634+ <SubmitButton variant="quiet" match={{ action: "labels" }} pending="Saving…">
635635 Save labels
636− </Button>
636+ </SubmitButton>
637637 </Form>
638638 </details>
639639 )}
+6−6
11 import { env } from "cloudflare:workers";
22 import { GitPullRequest, MessageSquare, Plus, Sparkles, X } from "lucide-react";
3−import { Form, Link, useNavigation, useRouteLoaderData } from "react-router";
3+import { Form, Link, useRouteLoaderData } from "react-router";
44
55 import type { Route } from "./+types/issues";
66 import { page } from "../../lib/meta";
7−import { Button, ButtonLink, ComputeNote, EmptyState, ErrorText, TimeAgo } from "../../components/ui";
7+import { ButtonLink, ComputeNote, EmptyState, ErrorText, SubmitButton, TimeAgo } from "../../components/ui";
88 import { Checkbox } from "../../components/ui/checkbox";
99 import {
1010 Assignee,
9191 const base = `${repo}/issues`;
9292 const stateQuery = state === "closed" ? "state=closed" : "";
9393 const assignable = agentsEnabled && state === "open" && issues.length > 0;
94− const assigning = useNavigation().state === "submitting";
94+
9595 // An archived repository's issues are locked: no new ones.
9696 const layout = useRouteLoaderData("routes/repo/layout") as { repo?: { archivedAt?: string | null } } | undefined;
9797 const archived = Boolean(layout?.repo?.archivedAt);
138138 Tick the issues to hand over. g1t takes each one in a run of its own,
139139 and they all work at once.
140140 </p>
141− <Button variant="accent" type="submit" disabled={assigning}>
142− {assigning ? "Starting sandboxes…" : "Assign to g1t"}
143− </Button>
141+ <SubmitButton variant="accent" pending="Starting sandboxes…">
142+ Assign to g1t
143+ </SubmitButton>
144144 {loaderData.computeNote && (
145145 <div className="basis-full">
146146 <ComputeNote note={loaderData.computeNote} />
+10−5
2020 RotateCw,
2121 } from "lucide-react";
2222 import { type ReactNode, Suspense } from "react";
23−import { Await, Form, Link, useNavigation } from "react-router";
23+import { Await, Form, Link } from "react-router";
2424
2525 import {
2626 type AgentRun,
4646 import { ProductionShot } from "../../components/production-shot";
4747 import { GithubLinkStrip } from "../../components/github";
4848 import { githubApp } from "../../lib/github.server";
49−import { Avatar, Button, ButtonLink, CopyLine, TimeAgo } from "../../components/ui";
49+import { Avatar, ButtonLink, CopyLine, SubmitButton, TimeAgo } from "../../components/ui";
5050 import { ChangeSize } from "../../components/work";
5151 import {
5252 type ActivityItem,
664664 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
665665 const previews = live.filter((app) => app.kind === "preview");
666666 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
667− const busy = useNavigation().state === "submitting";
668667 const addresses = useAddresses();
669668 const source = project?.source.kind === "hosted" ? project.source : null;
670669 const moving = agentsLive.length > 0 || columns.working.length + columns.checking.length > 0;
748747 )}
749748 {settings?.enabled ? (
750749 loaderData.can.run && <Form method="post">
751− <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
750+ <SubmitButton
751+ variant="quiet"
752+ name="intent"
753+ value="redeploy"
754+ pending="Redeploying…"
755+ title="Build production again from the default branch"
756+ >
752757 <RotateCw size={14} />
753758 Redeploy
754− </Button>
759+ </SubmitButton>
755760 </Form>
756761 ) : loaderData.can.manage_integrations && (
757762 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
+9−8
11 import { env } from "cloudflare:workers";
22 import type { G1tEvent } from "@g1t/contracts";
33 import { ArrowRight, CircleCheck, FileCode2, Sparkles } from "lucide-react";
4−import { Form, Link, data, useNavigation } from "react-router";
4+import { Form, Link, data } from "react-router";
55
66 import type { Route } from "./+types/plan";
77 import { refusal, requireRepo } from "../../lib/access.server";
1010 import { Markdown } from "../../components/markdown";
1111 import { Activity, Exchanges } from "../../components/activity";
1212 import { Outcome } from "../../components/outcome";
13−import { Button, ErrorText, TimeAgo } from "../../components/ui";
13+import { ErrorText, SubmitButton, TimeAgo, usePending } from "../../components/ui";
1414 import { Checkbox } from "../../components/ui/checkbox";
1515 import { Label } from "../../components/work";
1616 import { billing, events, identity, work } from "../../lib/services.server";
9191 export default function PlanPage({ loaderData, actionData, params }: Route.ComponentProps) {
9292 const { plan, costMicros, activity } = loaderData;
9393 const base = `/${params.owner}/${params.repo}`;
94− const applying = useNavigation().state === "submitting";
94+ // Either way of applying it, so the other button waits too.
95+ const applying = usePending();
9596
9697 // The agent is still writing it.
9798 const planning = plan.status === "planning";
262263
263264 {plan.status === "ready" && (
264265 <div className="mt-6 flex flex-wrap items-center gap-3">
265− <Button variant="accent" type="submit" name="action" value="assign" disabled={applying || !loaderData.can.run} title={whyNot(loaderData.can, "run")}>
266+ <SubmitButton variant="accent" name="action" value="assign" pending="Opening issues…" disabled={applying || !loaderData.can.run} title={whyNot(loaderData.can, "run")}>
266267 <Sparkles size={15} />
267− {applying ? "Opening issues…" : "Open these and assign g1t"}
268− </Button>
269− <Button variant="quiet" type="submit" name="action" value="open" disabled={applying || !loaderData.can.run} title={whyNot(loaderData.can, "run")}>
268+ Open these and assign g1t
269+ </SubmitButton>
270+ <SubmitButton variant="quiet" name="action" value="open" pending="Opening issues…" disabled={applying || !loaderData.can.run} title={whyNot(loaderData.can, "run")}>
270271 Only open the issues
271− </Button>
272+ </SubmitButton>
272273 <span className="text-xs text-muted">
273274 Untick any you do not want. Agents work on the independent ones at once
274275 and the rest follow as what they depend on merges.
+6−6
11 import { env } from "cloudflare:workers";
22 import { Sparkles } from "lucide-react";
3−import { Form, Link, redirect, useNavigation } from "react-router";
3+import { Form, Link, redirect } from "react-router";
44
55 import type { Plan } from "@g1t/contracts";
66
88 import { refusal, requireRepo } from "../../lib/access.server";
99 import { whyNot } from "../../lib/access";
1010 import { page } from "../../lib/meta";
11−import { Button, ComputeNote, EmptyState, ErrorText, Textarea, TimeAgo } from "../../components/ui";
11+import { ComputeNote, EmptyState, ErrorText, SubmitButton, Textarea, TimeAgo } from "../../components/ui";
1212 import { computeNoteFor } from "../../lib/compute.server";
1313 import { work } from "../../lib/services.server";
1414 import {
5858 export default function Plans({ loaderData, actionData, params }: Route.ComponentProps) {
5959 const { plans } = loaderData;
6060 const base = `/${params.owner}/${params.repo}`;
61− const starting = useNavigation().state === "submitting";
61+
6262 return (
6363 <div className="grid gap-10 lg:grid-cols-[1fr_20rem]">
6464 <div className="min-w-0">
7979 }
8080 />
8181 <div className="flex flex-wrap items-center gap-3">
82− <Button variant="accent" type="submit" disabled={starting || !loaderData.can.run} title={whyNot(loaderData.can, "run")}>
82+ <SubmitButton variant="accent" pending="Starting the planner…" disabled={!loaderData.can.run} title={whyNot(loaderData.can, "run")}>
8383 <Sparkles size={15} />
84− {starting ? "Starting the planner…" : "Plan it"}
85− </Button>
84+ Plan it
85+ </SubmitButton>
8686 <span className="text-xs text-muted">
8787 Nothing is opened until you have read the plan.
8888 </span>
+2−2
66 import { cloneUrl, useAddresses } from "../../lib/addresses";
77 import { Combobox } from "../../components/ui/combobox";
88 import {
9− Button,
9+ SubmitButton,
1010 CopyLine,
1111 EmptyState,
1212 ErrorText,
120120 <Input name="issue" type="number" min={1} defaultValue={issue} placeholder="12" />
121121 </Field>
122122 <ErrorText>{actionData?.error}</ErrorText>
123− <Button type="submit">Open pull request</Button>
123+ <SubmitButton pending="Opening…">Open pull request</SubmitButton>
124124 </Form>
125125 );
126126 }
+41−32
2424 Wrench,
2525 } from "lucide-react";
2626 import { Suspense } from "react";
27−import { Await, Form, Link, redirect, useNavigation } from "react-router";
27+import { Await, Form, Link, redirect } from "react-router";
2828
2929 import {
3030 type Capability,
4848 import { Markdown } from "../../components/markdown";
4949 import {
5050 Avatar,
51− Button,
5251 ButtonLink,
5352 CopyLine,
5453 EmptyState,
5554 ComputeNote,
5655 ErrorText,
56+ SubmitButton,
5757 Textarea,
5858 TimeAgo,
59+ usePending,
5960 } from "../../components/ui";
6061 import { CheckboxOption } from "../../components/ui/checkbox";
6162 import { WorkflowStatuses } from "../../components/actions";
584585 ];
585586 // A catch-up: the click shows at once; the answer says whether it is
586587 // done already or a sandbox is on it.
587− const navigation = useNavigation();
588− const catchUpPending = navigation.state !== "idle" && navigation.formData?.get("action") === "update";
588+ const catchUpPending = usePending({ action: "update" });
589589 const catchUp = actionData?.action === "update" ? actionData : null;
590590 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
591591 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
770770 {canRun &&
771771 pull.runtime === "hosted" &&
772772 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
773− <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
773+ // Keyed by the messages so the box empties once one shows below.
774+ <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30" key={messages.length}>
774775 <p className="flex items-center gap-2 text-sm font-medium">
775776 <Sparkles size={15} className="text-merged" />
776777 Message the agent
789790 placeholder="Keep the old flag working too…"
790791 className="h-9 min-w-0 grow rounded-md bg-bg px-3 text-sm ring-1 ring-line outline-none placeholder:text-faint focus:ring-merged/60"
791792 />
792− <Button type="submit">Send</Button>
793+ <SubmitButton match={{ action: "message" }} pending="Sending…">
794+ Send
795+ </SubmitButton>
793796 </div>
794797 </Form>
795798 )}
912915 Edit description
913916 </summary>
914917 <Form method="post" className="mt-3 space-y-2">
918+ <input type="hidden" name="action" value="describe" />
915919 <Textarea
916920 name="summary"
917921 rows={6}
918922 placeholder="What changed and why"
919923 defaultValue={pull.body ?? ""}
920924 />
921− <Button type="submit">Save</Button>
925+ <SubmitButton match={{ action: "describe" }} pending="Saving…">
926+ Save
927+ </SubmitButton>
922928 </Form>
923929 </details>
924930 )}
936942 </StatusRow>
937943 {canManage && (
938944 <Form method="post" className="space-y-2 px-4 py-3">
945+ <input type="hidden" name="action" value="ready" />
939946 <Textarea name="summary" rows={3} placeholder="What changed and why" />
940− <Button type="submit">Mark ready for review</Button>
947+ <SubmitButton match={{ action: "ready" }} pending="Marking ready…">
948+ Mark ready for review
949+ </SubmitButton>
941950 </Form>
942951 )}
943952 </StatusBox>
10221031 {canUpdate && (
10231032 <Form method="post" className="mt-2">
10241033 <input type="hidden" name="action" value="update" />
1025− <Button variant="quiet" type="submit" disabled={catchUpPending}>
1026− {catchUpPending && <Loader size={14} className="animate-spin" />}
1027− {catchUpPending ? `Merging ${defaultBranch} in…` : `Catch up with ${defaultBranch} now`}
1028− </Button>
1034+ <SubmitButton variant="quiet" match={{ action: "update" }} pending={`Merging ${defaultBranch} in…`}>
1035+ Catch up with {defaultBranch} now
1036+ </SubmitButton>
10291037 </Form>
10301038 )}
10311039 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
10621070 <Layers size={15} />
10631071 See the queue
10641072 </ButtonLink>
1065− <Button variant="quiet" type="submit" name="action" value="unqueue">
1073+ <SubmitButton variant="quiet" name="action" value="unqueue" pending="Removing…">
10661074 Remove from the queue
1067− </Button>
1075+ </SubmitButton>
10681076 </Form>
10691077 )}
10701078 {canMerge && !landing && lifecycle?.stage !== "queued" && (
10841092 />
10851093 )}
10861094 <div className="flex flex-wrap items-center gap-3">
1087− <Button
1095+ <SubmitButton
10881096 variant="accent"
1089− type="submit"
10901097 name="action"
10911098 value="merge"
10921099 disabled={mergeBlocked != null}
10931100 title={mergeBlocked ?? undefined}
1101+ pending={mergeQueue ? "Adding to the queue…" : "Merging…"}
10941102 >
10951103 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
10961104 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1097− </Button>
1105+ </SubmitButton>
10981106 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
10991107 {mergeBlocked ?? (mergeQueue
11001108 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
11181126 />
11191127 {canManage && active && (
11201128 <Form method="post" className="flex justify-end">
1121− <Button variant="quiet" type="submit" name="action" value="close">
1129+ <SubmitButton variant="quiet" name="action" value="close" pending="Closing…">
11221130 Close pull request
1123− </Button>
1131+ </SubmitButton>
11241132 </Form>
11251133 )}
11261134 {actionData &&
1127− !["merge", "comment", "rerun-failed", "rerun-workflow", "update", "agent-review", "reviewers", "assign"].includes(
1135+ !["merge", "comment", "stack", "rerun-failed", "rerun-workflow", "update", "agent-review", "reviewers", "assign"].includes(
11281136 String(actionData.action),
11291137 ) && <ErrorText>{actionData.error}</ErrorText>}
11301138 </div>
11611169 <Form method="post" className="mt-3">
11621170 <input type="hidden" name="action" value="stack" />
11631171 <input type="hidden" name="branch" value={preview.branch} />
1164− <button
1165− type="submit"
1166− className="w-full rounded-md border border-line px-3 py-1.5 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg"
1172+ <SubmitButton
1173+ match={{ action: "stack" }}
1174+ pending="Starting the builds…"
1175+ className="flex w-full items-center justify-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg disabled:opacity-50"
11671176 >
11681177 Preview them against this change
1169− </button>
1178+ </SubmitButton>
11701179 </Form>
11711180 )}
11721181 {actionData?.action === "stack" &&
12241233 <input key={name} type="hidden" name="reviewer" value={name} />
12251234 ))}
12261235 <div className="*:w-full">
1227− <Button variant="quiet" type="submit">
1236+ <SubmitButton variant="quiet" match={{ action: "agent-review" }} pending="Asking g1t…">
12281237 <Sparkles size={14} className="text-accent" />
12291238 Request review from g1t
1230− </Button>
1239+ </SubmitButton>
12311240 </div>
12321241 <ComputeNote note={loaderData.computeNote} />
12331242 </Form>
12461255 members={members.filter((name) => name !== workOwner(pull).username)}
12471256 chosen={pull.reviewers.filter((name) => name !== "g1t")}
12481257 />
1249− <Button variant="quiet" type="submit">
1258+ <SubmitButton variant="quiet" match={{ action: "reviewers" }} pending="Saving…">
12501259 Save reviewers
1251− </Button>
1260+ </SubmitButton>
12521261 </Form>
12531262 </details>
12541263 </div>
12811290 ))}
12821291 <input type="hidden" name="assignee" value={viewer.username} />
12831292 <div className="*:w-full">
1284− <Button variant="quiet" type="submit">
1293+ <SubmitButton variant="quiet" name="who" value="self" pending="Assigning…">
12851294 Assign yourself
1286− </Button>
1295+ </SubmitButton>
12871296 </div>
12881297 </Form>
12891298 )}
12941303 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
12951304 <input type="hidden" name="action" value="assign" />
12961305 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
1297− <Button variant="quiet" type="submit">
1306+ <SubmitButton variant="quiet" name="who" value="picked" pending="Saving…">
12981307 Save assignees
1299− </Button>
1308+ </SubmitButton>
13001309 </Form>
13011310 </details>
13021311 </div>