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

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