1"use client"; 2 3/** 4 * PageCanvas — the route-level branch between the page surfaces. 5 * 6 * A page can be a plain block page, a database, or a whiteboard. The Page 7 * model has no reliable `type` field: a page IS a database when it owns a 8 * Database row (`GET /pages/:id` → `databaseId`, non-null ⇔ database), and a 9 * whiteboard when `kind === "whiteboard"` (T-144/T-167 — the API emits `kind` 10 * on the page). The [id] layout has already loaded the page (usePage) before 11 * the route renders, so `usePage` here is a cache hit (no extra fetch). 12 * 13 * - database page → <DatabasePageView> (full toolbar + view router). This is 14 * the branch the databases/[id] route comment had flagged to the core 15 * agent for exactly this route ("should branch on page.databaseId"); 16 * without it a database page opened on the block canvas and its seeded 17 * `database` block — which carries no `properties.databaseId` — rendered 18 * "Database not linked", leaving no reachable table to rename columns in. 19 * - whiteboard page → <WhiteboardCanvas> (T-168 hand-rolled SVG renderer). 20 * - plain page → <PageView> (the block editor). 21 * 22 * Renaming a database page's title in DatabasePageView PATCHes the database; 23 * the API mirrors that onto the owning page (database.service update → 24 * page.updateMany), so the sidebar tree (which reads the page title) stays 25 * in sync. 26 */ 27import { useEffect, useRef } from "react"; 28import { useParams } from "next/navigation"; 29import { usePage } from "@/hooks/use-pages"; 30import { useWorkspace } from "@/hooks/use-workspace"; 31import { meApi } from "@/lib/api"; 32import { PageView } from "./page-view"; 33import { DatabasePageView } from "@/components/database/database-page-view"; 34import { WhiteboardCanvas } from "@/components/whiteboard/whiteboard-canvas"; 35import { PageSpinner } from "./page-spinner"; 36 37export interface PageCanvasProps { 38 pageId: string; 39 /** Deep-link focus target (the /page/:id/:block route). */ 40 focusBlockId?: string | null; 41} 42 43export function PageCanvas({ pageId, focusBlockId }: PageCanvasProps) { 44 const params = useParams<{ workspace: string }>(); 45 const workspaceSlug = params?.workspace ?? ""; 46 const { data: page, isLoading } = usePage(pageId); 47 const { data: workspace } = useWorkspace(workspaceSlug); 48 49 // T-153 — record the open as a recently-VIEWED item, keyed by kind. Fire-and- 50 // forget (a failed record must never block the editor). Database pages are 51 // recorded by DatabasePageView itself (the dedicated /databases/:id route 52 // reaches it without this branch), so this only handles the non-database 53 // kinds here. `pageId` is the effect's key so a deep-link focus change that 54 // keeps the same route still re-records. 55 const recordedFor = useRef<string | null>(null); 56 useEffect(() => { 57 if (!page || page.databaseId) return; 58 const key = `${page.kind ?? "page"}:${pageId}`; 59 if (recordedFor.current === key) return; 60 recordedFor.current = key; 61 const kind = page.kind === "whiteboard" ? "whiteboard" : page.kind === "task" ? "task" : "page"; 62 void meApi.recordView(kind, pageId).catch(() => undefined); 63 }, [page, pageId]); 64 65 if (isLoading || !page) return <PageSpinner />; 66 67 const readOnly = (workspace?.role ?? "").toLowerCase() === "guest"; 68 69 // Whiteboard page (T-167 kind) → the canvas surface (T-168). Checked before 70 // the database branch: a whiteboard page has no Database row, so the order 71 // is only for clarity — but kind is the explicit marker. 72 if (page.kind === "whiteboard") { 73 return ( 74 <div className="h-full"> 75 <WhiteboardCanvas pageId={pageId} workspaceId={page.workspaceId} readOnly={readOnly} /> 76 </div> 77 ); 78 } 79 80 // Database page → the dedicated database surface (not the block editor). 81 if (page.databaseId) { 82 return ( 83 <div className="h-full"> 84 <DatabasePageView 85 databaseId={page.databaseId} 86 workspaceId={page.workspaceId} 87 workspaceSlug={workspace?.slug ?? workspaceSlug} 88 readOnly={readOnly} 89 /> 90 </div> 91 ); 92 } 93 94 return <PageView pageId={pageId} focusBlockId={focusBlockId} />; 95} 96