Skip to content

g1t/apps/web/app/routes/workspace/ruleset.tsx

87 lines3,708 bytesCodeBlame
1import { ArrowLeft } from "lucide-react";
2import { Link, data, redirect } from "react-router";
3
4import type { RulesetSpec } from "@g1t/contracts";
5
6import type { Route } from "./+types/ruleset";
7import { page } from "../../lib/meta";
8import { EnforcementBadge, RulesetForm } from "../../components/rules";
9import { newRuleset } from "../../lib/rules";
10import { TimeAgo } from "../../components/ui";
11import { work } from "../../lib/services.server";
12import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
13
14export function meta({ params, loaderData, ...args }: Route.MetaArgs) {
15 const name = loaderData?.existing?.name ?? "New ruleset";
16 return page(args, { title: `${name} · Rules · ${params.owner} · g1t` });
17}
18
19export async function loader({ params, context }: Route.LoaderArgs) {
20 const viewer = getViewer(context);
21 const role = roleIn(viewer, params.owner);
22 if (!role) throw data(null, { status: 404 });
23 const editable = role === "owner";
24 if (params.id === "new") {
25 if (!editable) throw data("Only owners of the workspace can create its rulesets.", { status: 403 });
26 return { existing: null, editable };
27 }
28 const found = await work.getRuleset({ workspace: params.owner }, params.id, viewer);
29 if (!found.ok && found.error.code === "not_found") throw data(null, { status: 404 });
30 return { existing: unwrap(found), editable };
31}
32
33export async function action({ request, params, context }: Route.ActionArgs) {
34 assertSameOrigin(request);
35 const user = requireUser(context, request);
36 const form = await request.formData();
37 const owner = { workspace: params.owner };
38 const list = `/${params.owner}/-/rules`;
39 if (form.get("intent") === "delete" && params.id !== "new") {
40 const deleted = await work.deleteRuleset(user, owner, params.id);
41 return deleted.ok ? redirect(list) : { error: deleted.error.message };
42 }
43 let ruleset: RulesetSpec;
44 try {
45 ruleset = JSON.parse(String(form.get("ruleset") ?? "")) as RulesetSpec;
46 } catch {
47 return { error: "The ruleset could not be read. Reload the page and try again." };
48 }
49 const saved = await work.saveRuleset(user, owner, ruleset, params.id === "new" ? undefined : params.id);
50 return saved.ok ? redirect(list) : { error: saved.error.message };
51}
52
53export default function WorkspaceRuleset({ loaderData, actionData, params }: Route.ComponentProps) {
54 const { existing, editable } = loaderData;
55 const list = `/${params.owner}/-/rules`;
56 return (
57 <div className="mx-auto max-w-4xl">
58 <header className="mb-6 border-b border-line pb-5">
59 <Link to={list} className="mb-3 inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
60 <ArrowLeft size={14} /> Rules
61 </Link>
62 <h1 className="flex flex-wrap items-center gap-2 text-lg font-semibold tracking-tight">
63 {existing ? existing.name : "New ruleset"}
64 {existing && <EnforcementBadge enforcement={existing.enforcement} />}
65 </h1>
66 <p className="mt-1 text-sm text-muted">
67 {existing ? (
68 <>
69 Changed <TimeAgo at={existing.updated_at} /> by <span className="font-mono">{existing.updated_by}</span>.
70 </>
71 ) : (
72 `Rules for the branches or tags of ${params.owner}'s repositories, all or some.`
73 )}
74 </p>
75 </header>
76 <RulesetForm
77 key={existing?.id ?? "new"}
78 initial={existing ?? newRuleset("workspace")}
79 level="workspace"
80 existing={existing ?? undefined}
81 editable={editable}
82 error={actionData && "error" in actionData ? actionData.error : null}
83 backHref={list}
84 />
85 </div>
86 );
87}