"use client"; /** * PageCanvas — the route-level branch between the page surfaces. * * A page can be a plain block page, a database, or a whiteboard. The Page * model has no reliable `type` field: a page IS a database when it owns a * Database row (`GET /pages/:id` → `databaseId`, non-null ⇔ database), and a * whiteboard when `kind === "whiteboard"` (T-144/T-167 — the API emits `kind` * on the page). The [id] layout has already loaded the page (usePage) before * the route renders, so `usePage` here is a cache hit (no extra fetch). * * - database page → (full toolbar + view router). This is * the branch the databases/[id] route comment had flagged to the core * agent for exactly this route ("should branch on page.databaseId"); * without it a database page opened on the block canvas and its seeded * `database` block — which carries no `properties.databaseId` — rendered * "Database not linked", leaving no reachable table to rename columns in. * - whiteboard page → (T-168 hand-rolled SVG renderer). * - plain page → (the block editor). * * Renaming a database page's title in DatabasePageView PATCHes the database; * the API mirrors that onto the owning page (database.service update → * page.updateMany), so the sidebar tree (which reads the page title) stays * in sync. */ import { useEffect, useRef } from "react"; import { useParams } from "next/navigation"; import { usePage } from "@/hooks/use-pages"; import { useWorkspace } from "@/hooks/use-workspace"; import { meApi } from "@/lib/api"; import { PageView } from "./page-view"; import { DatabasePageView } from "@/components/database/database-page-view"; import { WhiteboardCanvas } from "@/components/whiteboard/whiteboard-canvas"; import { PageSpinner } from "./page-spinner"; export interface PageCanvasProps { pageId: string; /** Deep-link focus target (the /page/:id/:block route). */ focusBlockId?: string | null; } export function PageCanvas({ pageId, focusBlockId }: PageCanvasProps) { const params = useParams<{ workspace: string }>(); const workspaceSlug = params?.workspace ?? ""; const { data: page, isLoading } = usePage(pageId); const { data: workspace } = useWorkspace(workspaceSlug); // T-153 — record the open as a recently-VIEWED item, keyed by kind. Fire-and- // forget (a failed record must never block the editor). Database pages are // recorded by DatabasePageView itself (the dedicated /databases/:id route // reaches it without this branch), so this only handles the non-database // kinds here. `pageId` is the effect's key so a deep-link focus change that // keeps the same route still re-records. const recordedFor = useRef(null); useEffect(() => { if (!page || page.databaseId) return; const key = `${page.kind ?? "page"}:${pageId}`; if (recordedFor.current === key) return; recordedFor.current = key; const kind = page.kind === "whiteboard" ? "whiteboard" : page.kind === "task" ? "task" : "page"; void meApi.recordView(kind, pageId).catch(() => undefined); }, [page, pageId]); if (isLoading || !page) return ; const readOnly = (workspace?.role ?? "").toLowerCase() === "guest"; // Whiteboard page (T-167 kind) → the canvas surface (T-168). Checked before // the database branch: a whiteboard page has no Database row, so the order // is only for clarity — but kind is the explicit marker. if (page.kind === "whiteboard") { return (
); } // Database page → the dedicated database surface (not the block editor). if (page.databaseId) { return (
); } return ; }