Skip to content

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

72 lines3,460 bytesCodeBlame
1import { ChevronLeft, Trash2 } from "lucide-react";
2import { Form, Link, data, redirect } from "react-router";
3
4import type { Route } from "./+types/release";
5import { ReleaseCard } from "../../components/releases";
6import { ErrorText, SubmitButton } from "../../components/ui";
7import { requireRepo } from "../../lib/access.server";
8import { page } from "../../lib/meta";
9import { repos } from "../../lib/services.server";
10import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server";
11
12export function meta({ params, loaderData, ...args }: Route.MetaArgs) {
13 const title = loaderData?.release.name || params["*"];
14 return page(args, { title: `${title} · ${params.owner}/${params.repo} · g1t` });
15}
16
17export async function loader({ params, context }: Route.LoaderArgs) {
18 const { viewer, access } = await requireRepo(context, params, "read");
19 const path = { namespace: params.owner, name: params.repo };
20 const release = unwrap(await repos.release(path, viewer, { tag: params["*"] ?? "" }));
21 return { release, canPush: access.can.push };
22}
23
24/** Publishing a draft, or deleting the release (its tag stays). */
25export async function action({ request, params, context }: Route.ActionArgs) {
26 assertSameOrigin(request);
27 const user = requireUser(context, request);
28 const form = await request.formData();
29 const path = { namespace: params.owner, name: params.repo };
30 const id = String(form.get("id") ?? "");
31 if (form.get("intent") === "publish") {
32 const published = await repos.updateRelease(user, path, id, { draft: false });
33 if (!published.ok) return data({ error: published.error.message }, { status: 400 });
34 return { error: null };
35 }
36 if (form.get("intent") !== "delete") return data({ error: "Nothing to do." }, { status: 400 });
37 const deleted = await repos.deleteRelease(user, path, id);
38 if (!deleted.ok) return data({ error: deleted.error.message }, { status: 400 });
39 throw redirect(`/${params.owner}/${params.repo}/releases`);
40}
41
42export default function ReleasePage({ loaderData, actionData, params }: Route.ComponentProps) {
43 const { release, canPush } = loaderData;
44 const base = `/${params.owner}/${params.repo}`;
45 return (
46 <div className="space-y-6">
47 <div className="flex flex-wrap items-center justify-between gap-3">
48 <Link to={`${base}/releases`} className="inline-flex items-center gap-1 text-sm text-muted hover:text-accent">
49 <ChevronLeft size={15} />
50 Releases
51 </Link>
52 {canPush && (
53 <Form method="post" className="flex items-center gap-2">
54 <input type="hidden" name="id" value={release.id} />
55 {release.draft && (
56 <SubmitButton name="intent" value="publish" pending="Publishing…">
57 Publish release
58 </SubmitButton>
59 )}
60 <SubmitButton variant="danger" name="intent" value="delete" pending="Deleting…">
61 <Trash2 size={14} />
62 Delete release
63 </SubmitButton>
64 </Form>
65 )}
66 </div>
67 {actionData?.error && <ErrorText>{actionData.error}</ErrorText>}
68 <ReleaseCard release={release} repo={{ namespace: params.owner, name: params.repo }} linkTitle={false} />
69 {canPush && <p className="text-xs text-faint">Deleting a release keeps its tag. Delete the tag with git: <code className="font-mono">git push origin :refs/tags/{release.tagName}</code></p>}
70 </div>
71 );
72}