Skip to content
104 linesCodeBlameRaw

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.

Docs: a workspace knowledge base people and agents write together1import type { DocTemplate } from "@g1t/contracts";
2import { useState } from "react";
3import { data, useNavigate } from "react-router";
4
5import type { Route } from "./+types/templates";
6import { docsRequest, useDocsAction, useDocsData } from "../../../components/docs/actions";
7import { SectionTitle, TemplateCard } from "../../../components/docs/parts";
8import { ErrorText } from "../../../components/ui";
9import { SelectField } from "../../../components/ui/select";
10import { canDo } from "../../../lib/docs";
11import { page } from "../../../lib/meta";
12import { docs } from "../../../lib/services.server";
13import { requireUser, roleIn } from "../../../lib/session.server";
14
15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Templates · Docs · ${params.owner} · g1t` });
17}
18
19export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ templates: DocTemplate[] }> {
20 const viewer = requireUser(context, request);
21 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
22 const found = await docs.templates(params.owner.toLowerCase(), viewer).catch(() => null);
23 return { templates: found?.ok ? found.value : [] };
24}
25
26/** g1t's templates and the workspace's own; start a page from one in any space you can write in. */
27export default function DocsTemplates({ loaderData, params }: Route.ComponentProps) {
28 const slug = params.owner.toLowerCase();
29 const layout = useDocsData();
30 const navigate = useNavigate();
31 const { send, error: actionError } = useDocsAction(slug);
32 const writable = (layout?.sidebar?.spaces ?? []).filter((s) => canDo(s.viewer_role, "edit"));
33 const [space, setSpace] = useState(writable.find((s) => s.is_default)?.id ?? writable[0]?.id ?? "");
34 const [error, setError] = useState<string | null>(null);
35 const use = async (template: string) => {
36 const made = await docsRequest<{ path: string }>(slug, "create_page", { page: { space_id: space, template_id: template } });
37 if (made.ok) navigate(made.value.path);
38 else setError(made.error.message);
39 };
40 const builtin = loaderData.templates.filter((t) => t.builtin);
41 const own = loaderData.templates.filter((t) => !t.builtin);
42 return (
43 <div className="mx-auto max-w-5xl">
44 <div className="flex flex-wrap items-end justify-between gap-4">
45 <div>
46 <h1 className="text-xl font-semibold tracking-tight">Templates</h1>
47 <p className="mt-1 text-sm text-muted">Start a page with its structure already there. Save any page as a template from its ⋯ menu.</p>
48 </div>
49 {writable.length > 0 && (
50 <label className="flex items-center gap-2 text-sm text-muted">
51 New pages go in
52 <SelectField aria-label="Space" value={space} onValueChange={setSpace} options={writable.map((s) => ({ value: s.id, label: s.name }))} className="h-9 min-w-40" />
53 </label>
54 )}
55 </div>
56 {(error || actionError) && (
57 <div className="mt-3">
58 <ErrorText>{error ?? actionError}</ErrorText>
59 </div>
60 )}
61 <section className="mt-8">
62 <SectionTitle>From g1t</SectionTitle>
63 <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
64 {builtin.map((t) => (
65 <TemplateCard
66 key={t.id}
67 template={t}
68 action={
69 <button type="button" disabled={!space} onClick={() => use(t.id)} className="text-xs font-medium text-accent hover:underline disabled:text-faint">
70 Use template
71 </button>
72 }
73 />
74 ))}
75 </div>
76 </section>
77 <section className="mt-10">
78 <SectionTitle>Your workspace&apos;s</SectionTitle>
79 {own.length === 0 ? (
80 <p className="text-sm text-muted">None yet. Open a page, then choose Save as template from its ⋯ menu.</p>
81 ) : (
82 <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
83 {own.map((t) => (
84 <TemplateCard
85 key={t.id}
86 template={t}
87 action={
88 <>
89 <button type="button" disabled={!space} onClick={() => use(t.id)} className="text-xs font-medium text-accent hover:underline disabled:text-faint">
90 Use template
91 </button>
92 <button type="button" onClick={() => send("delete_template", { template_id: t.id })} className="ml-auto text-xs text-faint hover:text-danger">
93 Delete
94 </button>
95 </>
96 }
97 />
98 ))}
99 </div>
100 )}
101 </section>
102 </div>
103 );
104}