"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Plus, GripVertical, Trash2 } from "lucide-react"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/primitives"; import { api } from "@/lib/client/api"; import { cn } from "@/lib/utils"; import type { Property, RowOut } from "@/lib/databases/types"; import { useDb } from "../hooks"; import { Cell } from "../cells/Cell"; import { TypeIcon } from "../meta"; import { PropertyEditor } from "../PropertyEditor"; import { groupRows } from "../group"; import { OPTION_CLASS } from "../colors"; const MIN_W = 80; export function TableView() { const ctx = useDb(); const cols = ctx.visibleProps; const widths = ctx.view.config.widths ?? {}; const [editing, setEditing] = useState(null); const [sel, setSel] = useState>(new Set()); const [editProp, setEditProp] = useState(null); const [adding, setAdding] = useState(false); const [localW, setLocalW] = useState>({}); const w = (p: Property) => localW[p.id] ?? widths[p.id] ?? p.width ?? (p.type === "title" ? 260 : 180); const gb = ctx.view.config.groupBy ? ctx.props.find((p) => p.id === ctx.view.config.groupBy) : undefined; const groups = groupRows(ctx.rows, gb); const canReorder = !gb && !(ctx.view.config.sorts?.length) && !ctx.search; const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); const flat = groups.flatMap((g) => g.rows); const nav = useCallback((e: React.KeyboardEvent) => { const el = (e.target as HTMLElement).closest("[data-cell]"); if (!el || editing) return; const [r, c] = el.dataset.cell!.split(":").map(Number); const go = (nr: number, nc: number) => { const t = el.closest("table")?.querySelector(`[data-cell="${nr}:${nc}"]`); if (t) { e.preventDefault(); t.focus(); } }; if (e.key === "ArrowDown") go(r + 1, c); else if (e.key === "ArrowUp") go(r - 1, c); else if (e.key === "ArrowRight" || (e.key === "Tab" && !e.shiftKey)) go(r, c + 1); else if (e.key === "ArrowLeft" || (e.key === "Tab" && e.shiftKey)) go(r, c - 1); else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); const row = flat[r], p = cols[c]; if (row && p) { if (p.type === "checkbox") ctx.setCell(row.id, { [p.id]: !row.values[p.id] }); else setEditing(`${row.id}:${p.id}`); } } else if (e.key === "Escape") setEditing(null); }, [editing, flat, cols, ctx]); const startResize = (p: Property, e: React.PointerEvent) => { e.preventDefault(); const x0 = e.clientX, w0 = w(p); let cur = w0; const move = (ev: PointerEvent) => { cur = Math.max(MIN_W, w0 + ev.clientX - x0); setLocalW((s) => ({ ...s, [p.id]: cur })); }; const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); ctx.patchView({ widths: { ...widths, [p.id]: cur } }); }; window.addEventListener("pointermove", move); window.addEventListener("pointerup", up); }; const onDragEnd = (e: DragEndEvent) => { const { active, over } = e; if (!over || active.id === over.id) return; const ids = flat.map((r) => r.id); const next = arrayMove(ids, ids.indexOf(String(active.id)), ids.indexOf(String(over.id))); api(`/api/databases/${ctx.db.id}/rows`, { method: "PATCH", json: { order: next } }).then(() => ctx.refresh()); }; useEffect(() => { setSel((s) => new Set([...s].filter((id) => flat.some((r) => r.id === id)))); }, [ctx.rows.length]); // eslint-disable-line react-hooks/exhaustive-deps let ri = 0; return (
{sel.size > 0 &&
{sel.size} selected
}
r.id)} strategy={verticalListSortingStrategy}> {cols.map((p, ci) => ( ))} {groups.map((g) => (g.rows.length || !gb ? [ gb && , ...g.rows.map((row) => setSel((s) => { const n = new Set(s); v ? n.add(row.id) : n.delete(row.id); return n; })} canReorder={canReorder} />), gb && , ] : null))} {!gb && }
setEditProp(o ? p.id : null)}> setEditProp(null)} />
startResize(p, e)} className="absolute right-0 top-0 z-[1] h-full w-2 cursor-col-resize touch-none opacity-0 hover:opacity-100 group-hover/h:opacity-100">
setAdding(false)} />
{g.label}{g.rows.length}
{ctx.total} row{ctx.total === 1 ? "" : "s"}{ctx.rows.length < ctx.total ? ` ยท showing ${ctx.rows.length}` : ""}
); } function Row({ row, ri, cols, w, editing, setEditing, selected, onSelect, canReorder }: { row: RowOut; ri: number; cols: Property[]; w: (p: Property) => number; editing: string | null; setEditing: (k: string | null) => void; selected: boolean; onSelect: (v: boolean) => void; canReorder: boolean }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: row.id, disabled: !canReorder }); return (
onSelect(e.target.checked)} className={cn("size-3.5 accent-[var(--accent)]", !selected && "opacity-0 group-hover/row:opacity-100")} aria-label="Select row" />{canReorder && }
{cols.map((p, ci) => { const key = `${row.id}:${p.id}`; return ( 0 && "border-l border-line")}>
setEditing(o ? key : null)} />
); })} ); }