Skip to content
68 linesCodeBlameRaw
1import { Outlet, data, type ShouldRevalidateFunctionArgs } from "react-router";
2
3import { type DocsSidebar as Sidebar, shownUsername } from "@g1t/contracts";
4
5import type { Route } from "./+types/layout";
6import type { DocMentionable } from "../../../components/docs/editor";
7import { workspacePeople } from "../../../lib/chat.server";
8import { docs } from "../../../lib/services.server";
9import { requireUser, roleIn } from "../../../lib/session.server";
10
11/**
12 * Docs mode: what its sidebar shows (the shell draws it from this data:
13 * spaces and their page trees, favorites, recent pages) and who can be
14 * mentioned in a page. Each part is empty rather than an error when its
15 * service does not answer.
16 */
17export type DocsLayoutData = {
18 slug: string;
19 role: string;
20 me: { key: string; name: string; display_name: string; avatar: string | null };
21 sidebar: Sidebar | null;
22 mentionables: DocMentionable[];
23};
24
25export async function loader({ params, context, request }: Route.LoaderArgs): Promise<DocsLayoutData> {
26 const viewer = requireUser(context, request);
27 const role = roleIn(viewer, params.owner);
28 if (!role) throw data(null, { status: 404 });
29 const slug = params.owner.toLowerCase();
30 const [sidebar, { people, agents }] = await Promise.all([
31 docs
32 .sidebar(slug, viewer)
33 .then((r) => (r.ok ? r.value : null))
34 .catch(() => null),
35 workspacePeople(slug, viewer).catch(() => ({ people: [], agents: [] })),
36 ]);
37 const self = people.find((p) => p.name === viewer.username.toLowerCase());
38 return {
39 slug,
40 role,
41 me: {
42 key: `user:${viewer.id}`,
43 name: viewer.username.toLowerCase(),
44 display_name: self?.display_name ?? shownUsername({ username: viewer.username, display_username: viewer.display_username ?? null }),
45 avatar: viewer.avatar ?? null,
46 },
47 sidebar,
48 mentionables: [
49 ...people.map((p): DocMentionable => ({ kind: "user", id: p.name, name: p.name, display_name: p.display_name, avatar: p.avatar })),
50 ...agents.map((a): DocMentionable => ({ kind: "agent", id: a.id, name: a.handle, display_name: a.display_name, avatar: a.avatar, avatar_seed: a.avatar_seed ?? null })),
51 ],
52 };
53}
54
55/**
56 * The tree changes when something is made, renamed, moved or trashed (a
57 * submission, or the page asking again with `useRevalidator`); not when
58 * moving between pages.
59 */
60export function shouldRevalidate({ currentParams, nextParams, currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
61 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
62 if (currentUrl.href === nextUrl.href) return defaultShouldRevalidate;
63 return currentParams.owner !== nextParams.owner;
64}
65
66export default function DocsLayout() {
67 return <Outlet />;
68}