flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

104 lines4,725 bytesCodeBlame
1import { Box, Code2, GitBranch } from "lucide-react";
2import { Form, Link, data, useNavigation } from "react-router";
3
4import type { Route } from "./+types/settings";
5import { page } from "../../lib/meta";
6import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
7import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui";
8import { projects } from "../../lib/services.server";
9import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
10
11export function meta({ params, ...args }: Route.MetaArgs) {
12 return page(args, { title: `Settings · ${params.owner}/${params.repo} · g1t` });
13}
14
15export async function loader({ params, context }: Route.LoaderArgs) {
16 const viewer = getViewer(context);
17 // Members only; to anyone else the page does not exist.
18 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
19 return { project: unwrap(await projects.get(params.owner, params.repo, viewer)) };
20}
21
22export async function action({ request, params, context }: Route.ActionArgs) {
23 assertSameOrigin(request);
24 const user = requireUser(context, request);
25 const form = await request.formData();
26 const saved = await projects.update(user, params.owner, params.repo, {
27 name: String(form.get("name") ?? ""),
28 description: String(form.get("description") ?? ""),
29 rootDir: String(form.get("rootDir") ?? ""),
30 });
31 return saved.ok ? { saved: true as const } : { error: saved.error.message };
32}
33
34export default function ProjectSettings({ loaderData, actionData, params }: Route.ComponentProps) {
35 const { project } = loaderData;
36 const saving = useNavigation().state === "submitting";
37 const base = `/${params.owner}/${params.repo}`;
38 const source = project.source.kind === "hosted" ? project.source : null;
39 return (
40 <div className="max-w-4xl">
41 <RepoSettingsTabs base={base} />
42 <Form method="post" className="space-y-8">
43 <section className="space-y-4">
44 <h2 className="flex items-center gap-2 text-sm font-medium">
45 <Box size={15} className="text-accent" />
46 Project
47 </h2>
48 <div className="grid gap-4 md:grid-cols-2">
49 <Field label="Name" hint={`Its address stays g1t.sh/${project.workspace}/${project.slug}.`}>
50 <Input name="name" defaultValue={project.name} required maxLength={100} />
51 </Field>
52 <Field label="Description">
53 <Input name="description" defaultValue={project.description ?? ""} maxLength={200} placeholder="What it is, in a line" />
54 </Field>
55 </div>
56 </section>
57
58 <section className="space-y-4">
59 <h2 className="flex items-center gap-2 text-sm font-medium">
60 <Code2 size={15} className="text-accent" />
61 Source
62 </h2>
63 {source && (
64 <div className="rounded-xl border border-line bg-surface p-4 text-sm">
65 <div className="flex flex-wrap items-center justify-between gap-3">
66 <div>
67 <Link to={`${base}/code`} className="font-mono hover:text-accent">
68 {source.repo.namespace}/{source.repo.name}
69 </Link>
70 <p className="mt-0.5 text-xs text-muted">
71 Hosted on g1t · <GitBranch size={11} className="inline" /> {source.defaultBranch}
72 </p>
73 </div>
74 <Link to={`${base}/settings/repository`} className="text-xs text-muted hover:text-fg">
75 Repository settings
76 </Link>
77 </div>
78 <p className="mt-3 text-xs text-faint">
79 Mirroring a repository from GitHub, GitLab or Bitbucket as a project's source is coming next.
80 </p>
81 </div>
82 )}
83 <Field
84 label="Root directory"
85 hint="Where in the repository this project lives, such as apps/web. Builds run there. Empty for the whole repository."
86 >
87 <Input name="rootDir" defaultValue={source?.rootDir ?? ""} placeholder="apps/web" className="font-mono" />
88 </Field>
89 </section>
90
91 <div className="flex items-center gap-3">
92 <Button type="submit" disabled={saving}>
93 Save
94 </Button>
95 {actionData && "saved" in actionData && <span className="text-sm text-accent">Saved.</span>}
96 <span className="ml-auto text-xs text-faint">
97 Created by {project.createdBy} <TimeAgo at={project.createdAt} />
98 </span>
99 </div>
100 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
101 </Form>
102 </div>
103 );
104}