"use client"; /** * RecordPanel — right-hand inspector for a record. * * Every non-title property renders as an editable row using the same * CellEditor used in table cells (one editor per type, Enter/blur commit). * The title is edited inline at the top. Metadata (created/edited by & time) * and the record page link live at the bottom. * * Opened from any view (table hover "eye", board card click, list row, * calendar day, timeline bar, gallery card). Closes on X / Escape / * clicking the backdrop. */ import { useEffect } from "react"; import { usePathname } from "next/navigation"; import { CalendarClock, Eye, ExternalLink, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { formatRelativeTime } from "@/lib/utils"; import { DatabaseProperty, RecordDto, pageUrlFromPath, useDatabaseRecord } from "@/hooks/use-database"; import { formatDateValue } from "@/components/database/cell-renderer"; import { CellEditor } from "@/components/database/cell-editor"; export interface RecordPanelProps { databaseId: string; record: RecordDto; properties: DatabaseProperty[]; onClose: () => void; readOnly?: boolean; } const SKIP_TYPES = new Set(["button", "created_time", "created_by", "last_edited_time", "last_edited_by", "unique_id"]); export function RecordPanel({ databaseId, record, properties, onClose, readOnly }: RecordPanelProps) { // Keep the record fresh (other editors / realtime may change it). const { data: fresh, isLoading } = useDatabaseRecord(databaseId, record.id); const r = fresh ?? record; const titleProp = properties.find((p) => p.isTitle) ?? properties.find((p) => p.type === "title"); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [onClose]); const editableProps = properties.filter((p) => !SKIP_TYPES.has(p.type) && p !== titleProp); const pathname = usePathname(); const openPage = () => { if (!r.pageId) return; // Page route is /w/[workspace]/page/[id] — the only page route (there is // no /p/{id} alias). Open in a new window (ExternalLink icon). window.open(pageUrlFromPath(pathname, r.pageId), "_blank", "noopener,noreferrer"); }; return ( <> {/* backdrop (click to close) */}
{/* title */}
{titleProp && ( undefined} className="text-base font-semibold" /> )}
{/* property rows */}
{isLoading &&

Loading…

} {editableProps.map((p) => (
{p.name} {p.required && *}
{readOnly ? ( {renderPlain(r, p)} ) : ( undefined} /> )}
))}
{/* metadata */}

Record · {r.id.slice(-8)}

Created {formatRelativeTime(r.createdTime)} {r.createdBy ? ` by ${r.createdBy.slice(-8)}` : ""}

Last edited {formatRelativeTime(r.lastEditedTime)}

v{r.version}

); } // Minimal plain-text fallback for read-only rows. function renderPlain(record: RecordDto, p: DatabaseProperty): string { const v = record.properties[p.id]; if (!v) return "—"; if (typeof v.text === "string") return v.text; if (typeof v.value === "number" || typeof v.value === "string" || typeof v.value === "boolean") return String(v.value); if (v.optionId) return v.optionId.slice(-6); if (v.optionIds?.length) return `${v.optionIds.length} selected`; if (v.userIds?.length) return `${v.userIds.length} person(s)`; if (v.recordIds?.length) return `${v.recordIds.length} linked`; if (v.fileIds?.length) return `${v.fileIds.length} file(s)`; if (v.start) return formatDateValue(v.start); if (v.value != null) return String(v.value); return "—"; }