"use client"; import { useEffect, useRef, useState } from "react"; import { Check, ExternalLink, Sparkles, RefreshCw, AlertCircle, Maximize2 } from "lucide-react"; import { Spinner, Tip } from "@/components/ui/primitives"; import { cn } from "@/lib/utils"; import type { Property, RowOut, RelationRef, FileRef } from "@/lib/databases/types"; import { isError, isPending, READONLY_TYPES } from "@/lib/databases/types"; import { displayValue, fmtDateValue, relativeDate, numberOf, optionOf, fmtNumber, isEmpty } from "@/lib/databases/values"; import { useDb } from "../hooks"; import { OptionChip, SelectPopover } from "./SelectCell"; import { RelationChip, RelationPopover } from "./RelationCell"; import { FileChip, FilePopover } from "./FileCell"; const toLocalInput = (v: unknown, time: boolean) => { if (typeof v !== "string" || !v) return ""; if (!time) return v.slice(0, 10); const d = new Date(v); if (Number.isNaN(d.getTime())) return ""; const z = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}T${z(d.getHours())}:${z(d.getMinutes())}`; }; /** Read-only rendering used by every view. */ export function CellDisplay({ p, value, row, compact }: { p: Property; value: unknown; row?: RowOut; compact?: boolean }) { if (isPending(value)) return generating…; if (isError(value)) return error; if (isEmpty(value)) return null; switch (p.type) { case "title": return {row?.icon ? `${row.icon} ` : ""}{String(value)}; case "checkbox": return {value ? : null}; case "select": case "status": { const o = optionOf(p, value); return o ? : {String(value)}; } case "multi_select": return {(Array.isArray(value) ? value : [value]).map((id) => { const o = optionOf(p, id); return ; })}; case "relation": return {(value as RelationRef[]).slice(0, compact ? 3 : 50).map((r) => )}{compact && (value as RelationRef[]).length > 3 && +{(value as RelationRef[]).length - 3}}; case "file": return {(value as FileRef[]).map((f) => )}; case "url": { const s = String(value); return e.stopPropagation()} className="inline-flex max-w-full items-center gap-1 truncate text-accent hover:underline">{s.replace(/^https?:\/\/(www\.)?/, "")}; } case "date": return {fmtDateValue(value, p.config.includeTime)}{!compact && {relativeDate(value)}}; case "created_time": case "last_edited_time": return {fmtDateValue(value, true)}; case "number": return {fmtNumber(numberOf(value), p.config.format)}; case "formula": return typeof value === "boolean" ? : {displayValue(p, value)}; case "rollup": return Array.isArray(value) ? {value.map((x, i) => {String(x)})} : {displayValue(p, value)}; case "ai": return {String(value)}; default: return {String(value)}; } } /** Editable cell (table + side peek). Controlled editing state so the table can drive it from the keyboard. */ export function Cell({ p, row, editing, onEdit, mode = "table" }: { p: Property; row: RowOut; editing: boolean; onEdit: (o: boolean) => void; mode?: "table" | "form" }) { const ctx = useDb(); const value = row.values[p.id]; const set = (v: unknown) => ctx.setCell(row.id, { [p.id]: v }); const readonly = READONLY_TYPES.has(p.type); const base = cn("flex min-h-9 w-full items-center gap-1 px-2 py-1 text-[13px]", mode === "form" && "min-h-10 rounded-md hover:bg-bg-sunk", !readonly && "cursor-text"); if (p.type === "checkbox") return ; if (p.type === "ai") return ; if (readonly) return
; if (p.type === "select" || p.type === "status" || p.type === "multi_select") return ctx.addOption(p.id, n)} open={editing} onOpenChange={onEdit}>
; if (p.type === "relation") return
; if (p.type === "file") return
; if (p.type === "date") return ; return ; } function TextCell({ p, row, value, editing, onEdit, onChange, className, mode }: { p: Property; row: RowOut; value: unknown; editing: boolean; onEdit: (o: boolean) => void; onChange: (v: unknown) => void; className: string; mode: "table" | "form" }) { const ctx = useDb(); const [draft, setDraft] = useState(""); const ref = useRef(null); useEffect(() => { if (editing) { setDraft(value == null ? "" : String(value)); setTimeout(() => { ref.current?.focus(); ref.current?.select?.(); }, 0); } }, [editing]); // eslint-disable-line react-hooks/exhaustive-deps const commit = () => { if (!editing) return; const v = p.type === "number" ? (draft.trim() === "" ? null : numberOf(draft)) : draft; if (v !== (value ?? (p.type === "number" ? null : ""))) onChange(v); onEdit(false); }; if (editing) { const common = { ref: ref as any, value: draft, onChange: (e: any) => setDraft(e.target.value), onBlur: commit, onKeyDown: (e: React.KeyboardEvent) => { if (e.key === "Enter" && !(e.shiftKey && mode === "form")) { e.preventDefault(); commit(); } if (e.key === "Escape") { e.preventDefault(); onEdit(false); } e.stopPropagation(); }, className: "w-full bg-transparent text-[13px] outline-none" }; return
{mode === "form" && p.type === "text" ?