pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/components/repo-lifecycle.tsx

150 lines5,231 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import { Archive } from "lucide-react";
2import { type ReactNode, useId, useState } from "react";
3import { Form, Link, useNavigation } from "react-router";
4
5import { Button, ErrorText } from "./ui";
6import {
7 AlertDialog,
8 AlertDialogCancel,
9 AlertDialogContent,
10 AlertDialogDescription,
11 AlertDialogFooter,
12 AlertDialogHeader,
13 AlertDialogTitle,
14} from "./ui/alert-dialog";
15import { FieldLabel, Field as FormField } from "./ui/field";
16import { Input as TextInput } from "./ui/input";
17import { confirmsName } from "../lib/repo-lifecycle";
18
19/**
20 * A dialog that posts one `intent`, says what will happen as a list, and,
21 * when `confirm` is given, asks for that name to be typed first. It opens
22 * from `trigger`, and opens by itself again when the action sent back an
23 * error, so the error is seen beside what caused it.
24 */
25export function ConfirmDialog({
26 intent,
27 fields,
28 title,
29 description,
30 children,
31 confirm,
32 submit,
33 busy,
34 error,
35 danger = true,
36 action,
37 trigger,
38}: {
39 intent: string;
40 /** Other hidden fields to post with it. */
41 fields?: Record<string, string>;
42 title: ReactNode;
43 description?: ReactNode;
44 /** What happens, as list items. */
45 children?: ReactNode;
46 /** The name to type, such as `acme/web`; nothing to type when absent. */
47 confirm?: string;
48 submit: string;
49 /** The button's words while it is posting. */
50 busy: string;
51 error?: string | null;
52 danger?: boolean;
53 /** Where to post, when not the page's own action. */
54 action?: string;
55 trigger: (open: () => void) => ReactNode;
56}) {
57 const [open, setOpen] = useState(Boolean(error));
58 const [typed, setTyped] = useState("");
59 const navigation = useNavigation();
60 const id = useId();
61 const posting =
62 navigation.state !== "idle" &&
63 navigation.formData?.get("intent") === intent &&
64 Object.entries(fields ?? {}).every(([k, v]) => navigation.formData?.get(k) === v);
65 const ready = confirm == null || confirmsName(typed, confirm);
66 return (
67 <>
68 {trigger(() => setOpen(true))}
69 <AlertDialog
70 open={open}
71 onOpenChange={(next) => {
72 setOpen(next);
73 setTyped("");
74 }}
75 >
76 <AlertDialogContent>
77 <Form method="post" action={action} className="grid gap-4">
78 <input type="hidden" name="intent" value={intent} />
79 {Object.entries(fields ?? {}).map(([name, value]) => (
80 <input key={name} type="hidden" name={name} value={value} />
81 ))}
82 <AlertDialogHeader>
83 <AlertDialogTitle>{title}</AlertDialogTitle>
84 {description && <AlertDialogDescription>{description}</AlertDialogDescription>}
85 </AlertDialogHeader>
86 {children && <ul className="list-disc space-y-1.5 pl-5 text-sm text-muted">{children}</ul>}
87 {confirm != null && (
88 <FormField>
89 <FieldLabel htmlFor={`${id}-confirm`}>
90 Type <span className="font-mono break-all text-fg">{confirm}</span> to confirm
91 </FieldLabel>
92 <TextInput
93 id={`${id}-confirm`}
94 name="confirm"
95 value={typed}
96 spellCheck={false}
97 autoCapitalize="off"
98 autoComplete="off"
99 onChange={(event) => setTyped(event.target.value)}
100 className="font-mono"
101 />
102 </FormField>
103 )}
104 <ErrorText>{error}</ErrorText>
105 <AlertDialogFooter>
106 <AlertDialogCancel type="button">Cancel</AlertDialogCancel>
107 <Button type="submit" variant={danger ? "danger" : "primary"} disabled={!ready || posting}>
108 {posting ? busy : submit}
109 </Button>
110 </AlertDialogFooter>
111 </Form>
112 </AlertDialogContent>
113 </AlertDialog>
114 </>
115 );
116}
117
118/**
119 * Said across the top of an archived repository's pages: what archived
120 * means, and for owners, where to undo it. On its settings pages it says
121 * why the settings are turned off.
122 */
123export function ArchivedBanner({ base, owner, settings }: { base: string; owner: boolean; settings?: boolean }) {
124 return (
125 <div
126 role="status"
127 className="flex flex-col gap-2 rounded-lg border border-warn/40 bg-warn/5 px-4 py-3 text-sm sm:flex-row sm:items-start sm:justify-between"
128 >
129 <p className="flex gap-2.5">
130 <Archive size={16} className="mt-0.5 shrink-0 text-warn" />
131 <span>
132 <span className="font-medium text-fg">This repository is archived.</span>{" "}
133 <span className="text-muted">
134 {settings
135 ? "It is read-only, so settings that change it are turned off until it is unarchived."
136 : "It is read-only: pushes and merges are refused, issues and pull requests are locked, and agents and workflows do not run. Its deployments keep serving."}
137 </span>
138 </span>
139 </p>
140 {owner && (
141 <Link
142 to={`${base}/settings/repository#danger-zone`}
143 className="shrink-0 pl-6.5 font-medium text-warn hover:underline sm:pl-0"
144 >
145 Unarchive
146 </Link>
147 )}
148 </div>
149 );
150}