Skip to content

Commit

Merge branch 'fast-push' into mirroring

syntaqxcommitted Parents9f9e123fbf8e2fBrowse files
3 files+70−220/3 viewed
+19−5
1313 return useRouteLoaderData("routes/workspace/docs/layout") as DocsLayoutData | undefined;
1414 }
1515
16+/** How long a Docs request may take before the page says so instead of waiting on. */
17+export const DOCS_TIMEOUT_MS = 20_000;
18+
19+/** What a request that failed or took too long says. */
20+export function docsFailure<T>(error: unknown): Result<T> {
21+ const late = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
22+ const message = late ? "Docs took too long to answer. Try again in a moment." : "Docs didn't answer. Check your connection and try again.";
23+ return { ok: false, error: { code: "conflict", message } };
24+}
25+
1626 export async function docsRequest<T>(slug: string, intent: string, body: Record<string, unknown> = {}): Promise<Result<T>> {
1727 try {
1828 const response = await fetch(`/${slug}/-/docs/api`, {
1929 method: "POST",
2030 headers: { "content-type": "application/json", accept: "application/json" },
2131 body: JSON.stringify({ intent, ...body }),
32+ signal: AbortSignal.timeout(DOCS_TIMEOUT_MS),
2233 });
2334 return (await response.json()) as Result<T>;
24− } catch {
25− return { ok: false, error: { code: "conflict", message: "Docs didn't answer. Check your connection and try again." } };
35+ } catch (error) {
36+ return docsFailure(error);
2637 }
2738 }
2839
2940 export async function docsQuery<T>(slug: string, query: Record<string, string>): Promise<Result<T>> {
3041 try {
31− const response = await fetch(`/${slug}/-/docs/api?${new URLSearchParams(query)}`, { headers: { accept: "application/json" } });
42+ const response = await fetch(`/${slug}/-/docs/api?${new URLSearchParams(query)}`, {
43+ headers: { accept: "application/json" },
44+ signal: AbortSignal.timeout(DOCS_TIMEOUT_MS),
45+ });
3246 return (await response.json()) as Result<T>;
33− } catch {
34− return { ok: false, error: { code: "conflict", message: "Docs didn't answer. Check your connection and try again." } };
47+ } catch (error) {
48+ return docsFailure(error);
3549 }
3650 }
3751
+27−7
5050 const { revalidate } = useRevalidator();
5151 const general = layout?.sidebar?.spaces.find((s) => s.is_default) ?? layout?.sidebar?.spaces.find((s) => canDo(s.viewer_role, "edit"));
5252 const spacesById = new Map((home?.spaces ?? []).map((s) => [s.id, s]));
53+ // What a new page is being started from ("" for a blank one), while it is.
54+ const [starting, setStarting] = useState<string | null>(null);
5355 const create = async (template?: string) => {
54− if (!general) return;
56+ if (!general || starting != null) return;
57+ setStarting(template ?? "");
58+ setError(null);
5559 const made = await docsRequest<{ path: string }>(slug, "create_page", { page: { space_id: general.id, template_id: template ?? null } });
56− if (made.ok) navigate(made.value.path);
57− else setError(made.error.message);
60+ if (made.ok) {
61+ await navigate(made.value.path);
62+ return;
63+ }
64+ setStarting(null);
65+ setError(made.error.message);
5866 };
5967 if (!home) {
6068 return (
8997 <Plus size={15} /> New space
9098 </ButtonLink>
9199 {general && (
92− <button type="button" onClick={() => create()} className="inline-flex h-9 items-center gap-1.5 rounded-md bg-accent px-3 text-sm font-medium text-bg transition-colors hover:bg-accent-hover">
93− <FilePlus2 size={15} /> New page
100+ <button
101+ type="button"
102+ disabled={starting != null}
103+ aria-busy={starting === ""}
104+ onClick={() => create()}
105+ className="inline-flex h-9 items-center gap-1.5 rounded-md bg-accent px-3 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-60"
106+ >
107+ <FilePlus2 size={15} /> {starting === "" ? "Starting…" : "New page"}
94108 </button>
95109 )}
96110 </div>
210224 key={t.id}
211225 template={t}
212226 action={
213− <button type="button" onClick={() => create(t.id)} className="text-xs font-medium text-accent hover:underline">
214− Use in {general.name}
227+ <button
228+ type="button"
229+ disabled={starting != null}
230+ aria-busy={starting === t.id}
231+ onClick={() => create(t.id)}
232+ className="text-xs font-medium text-accent hover:underline disabled:text-faint disabled:no-underline"
233+ >
234+ {starting === t.id ? "Starting…" : `Use in ${general.name}`}
215235 </button>
216236 }
217237 />
+24−10
3232 const writable = (layout?.sidebar?.spaces ?? []).filter((s) => canDo(s.viewer_role, "edit"));
3333 const [space, setSpace] = useState(writable.find((s) => s.is_default)?.id ?? writable[0]?.id ?? "");
3434 const [error, setError] = useState<string | null>(null);
35+ // The template a page is being started from, while it is.
36+ const [starting, setStarting] = useState<string | null>(null);
3537 const use = async (template: string) => {
38+ if (starting) return;
39+ setStarting(template);
40+ setError(null);
3641 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);
42+ if (made.ok) {
43+ await navigate(made.value.path);
44+ return;
45+ }
46+ setStarting(null);
47+ setError(made.error.message);
3948 };
49+ const startButton = (id: string) => (
50+ <button
51+ type="button"
52+ disabled={!space || starting != null}
53+ aria-busy={starting === id}
54+ onClick={() => use(id)}
55+ className="text-xs font-medium text-accent hover:underline disabled:text-faint disabled:no-underline"
56+ >
57+ {starting === id ? "Starting…" : "Use template"}
58+ </button>
59+ );
4060 const builtin = loaderData.templates.filter((t) => t.builtin);
4161 const own = loaderData.templates.filter((t) => !t.builtin);
4262 return (
6585 <TemplateCard
6686 key={t.id}
6787 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− }
88+ action={startButton(t.id)}
7389 />
7490 ))}
7591 </div>
86102 template={t}
87103 action={
88104 <>
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>
105+ {startButton(t.id)}
92106 <button type="button" onClick={() => send("delete_template", { template_id: t.id })} className="ml-auto text-xs text-faint hover:text-danger">
93107 Delete
94108 </button>