Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions templates/content/app/components/editor/DocumentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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}
>
<IconFileText />
Expand Down
35 changes: 35 additions & 0 deletions templates/content/app/components/editor/body-hydration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
databaseItemBodyHydrationIsPending,
documentBodyHydrationIsPending,
isEffectivelyEmptyDocumentContent,
newDocumentPageChoiceIsDisabled,
previewBodyHydrationIsPending,
previewBodyHydrationIsTerminalError,
previewDraftConflictsWithHydratedBody,
Expand Down Expand Up @@ -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({
Expand Down
10 changes: 10 additions & 0 deletions templates/content/app/components/editor/body-hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContentDatabaseItem, "bodyHydration" | "document">;
document: Pick<Document, "content" | "databaseMembership"> | null | undefined;
Expand Down
39 changes: 39 additions & 0 deletions templates/content/app/root.revalidation.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {},
): 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);
});
});
13 changes: 12 additions & 1 deletion templates/content/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
type: fixed
date: 2026-07-23
---

New pages open immediately while slow network work continues
Loading