diff --git a/templates/content/app/components/editor/DocumentEditor.tsx b/templates/content/app/components/editor/DocumentEditor.tsx index 55ee18867f..f876d38f79 100644 --- a/templates/content/app/components/editor/DocumentEditor.tsx +++ b/templates/content/app/components/editor/DocumentEditor.tsx @@ -85,6 +85,7 @@ import { cn } from "@/lib/utils"; import { documentBodyHydrationIsPending, isEffectivelyEmptyDocumentContent, + newDocumentPageChoiceIsDisabled, } from "./body-hydration"; import { BuilderBodySyncingNotice } from "./BuilderBodySyncingNotice"; import type { CommentTextAnchor } from "./comment-anchors"; @@ -1779,9 +1780,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) { type="button" variant="outline" className="justify-start gap-2" - disabled={ - !editorCanEdit || createDatabase.isPending - } + disabled={newDocumentPageChoiceIsDisabled({ + canEdit, + bodyHydrationPending, + databaseCreationPending: + createDatabase.isPending, + })} onClick={handleChoosePage} > diff --git a/templates/content/app/components/editor/body-hydration.test.ts b/templates/content/app/components/editor/body-hydration.test.ts index 14c395fc46..8736af0851 100644 --- a/templates/content/app/components/editor/body-hydration.test.ts +++ b/templates/content/app/components/editor/body-hydration.test.ts @@ -7,6 +7,7 @@ import { databaseItemBodyHydrationIsPending, documentBodyHydrationIsPending, isEffectivelyEmptyDocumentContent, + newDocumentPageChoiceIsDisabled, previewBodyHydrationIsPending, previewBodyHydrationIsTerminalError, previewDraftConflictsWithHydratedBody, @@ -291,6 +292,40 @@ describe("body hydration editing gates", () => { expect(isEffectivelyEmptyDocumentContent("Hydrated body")).toBe(false); }); + it("keeps the page choice usable while collaboration connects", () => { + expect( + newDocumentPageChoiceIsDisabled({ + canEdit: true, + bodyHydrationPending: false, + databaseCreationPending: false, + }), + ).toBe(false); + }); + + it("disables the page choice for viewers, body hydration, or database conversion", () => { + expect( + newDocumentPageChoiceIsDisabled({ + canEdit: false, + bodyHydrationPending: false, + databaseCreationPending: false, + }), + ).toBe(true); + expect( + newDocumentPageChoiceIsDisabled({ + canEdit: true, + bodyHydrationPending: true, + databaseCreationPending: false, + }), + ).toBe(true); + expect( + newDocumentPageChoiceIsDisabled({ + canEdit: true, + bodyHydrationPending: false, + databaseCreationPending: true, + }), + ).toBe(true); + }); + it("ignores untouched empty preview normalization before it can dirty-save", () => { expect( shouldIgnorePreviewEmptyNormalization({ diff --git a/templates/content/app/components/editor/body-hydration.ts b/templates/content/app/components/editor/body-hydration.ts index 4b0b11918c..4131abfa01 100644 --- a/templates/content/app/components/editor/body-hydration.ts +++ b/templates/content/app/components/editor/body-hydration.ts @@ -69,6 +69,16 @@ export function documentBodyHydrationIsPending( return builderBodyHydrationIsPending(hydration); } +export function newDocumentPageChoiceIsDisabled(args: { + canEdit: boolean; + bodyHydrationPending: boolean; + databaseCreationPending: boolean; +}) { + return ( + !args.canEdit || args.bodyHydrationPending || args.databaseCreationPending + ); +} + export function previewBodyHydrationIsPending(args: { item: Pick; document: Pick | null | undefined; diff --git a/templates/content/app/root.revalidation.test.ts b/templates/content/app/root.revalidation.test.ts new file mode 100644 index 0000000000..bbe2653b52 --- /dev/null +++ b/templates/content/app/root.revalidation.test.ts @@ -0,0 +1,39 @@ +import type { ShouldRevalidateFunctionArgs } from "react-router"; +import { describe, expect, it } from "vitest"; + +import { shouldRevalidate } from "./root"; + +function revalidationArgs( + overrides: Partial = {}, +): ShouldRevalidateFunctionArgs { + return { + currentUrl: new URL("https://content.test/page/previous"), + currentParams: { id: "previous" }, + nextUrl: new URL("https://content.test/page/next"), + nextParams: { id: "next" }, + defaultShouldRevalidate: true, + ...overrides, + } as ShouldRevalidateFunctionArgs; +} + +describe("root route revalidation", () => { + it("does not block ordinary page navigations on bootstrap locale data", () => { + expect(shouldRevalidate(revalidationArgs())).toBe(false); + }); + + it("retains React Router's default revalidation for action submissions", () => { + expect( + shouldRevalidate( + revalidationArgs({ formMethod: "POST", defaultShouldRevalidate: true }), + ), + ).toBe(true); + expect( + shouldRevalidate( + revalidationArgs({ + formMethod: "POST", + defaultShouldRevalidate: false, + }), + ), + ).toBe(false); + }); +}); diff --git a/templates/content/app/root.tsx b/templates/content/app/root.tsx index c85a6cb324..8e8b0229e3 100644 --- a/templates/content/app/root.tsx +++ b/templates/content/app/root.tsx @@ -49,7 +49,11 @@ import { useRouteLoaderData, useRouteError, } from "react-router"; -import type { LinksFunction, LoaderFunctionArgs } from "react-router"; +import type { + LinksFunction, + LoaderFunctionArgs, + ShouldRevalidateFunctionArgs, +} from "react-router"; // Styled sonner wrapper — passed via AppProviders `toaster` prop to avoid duplicate. import { Toaster as Sonner } from "@/components/ui/sonner"; @@ -105,6 +109,13 @@ export async function loader({ }; } +export function shouldRevalidate({ + defaultShouldRevalidate, + formMethod, +}: ShouldRevalidateFunctionArgs) { + return formMethod ? defaultShouldRevalidate : false; +} + // Pass args to match content's 3-way theme-cycle UX (no disableTransitionOnChange). const THEME_INIT_SCRIPT = getThemeInitScript("system", true); diff --git a/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md b/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md new file mode 100644 index 0000000000..72ec59a37e --- /dev/null +++ b/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md @@ -0,0 +1,6 @@ +--- +type: fixed +date: 2026-07-23 +--- + +New pages open immediately while slow network work continues