import { useState } from "react"; import { Text, Button, Group, Skeleton, Stack, Alert } from "@mantine/core"; import { IconAlertCircle, IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useViewData } from "../hooks/use-view-data"; import { useUpdateRow } from "../hooks/use-update-row"; import { useDatabaseRealtimeUpdates } from "../hooks/use-database-realtime-updates"; import { usePermissions } from "../hooks/use-permissions"; import { InlineEditor } from "../components/inline-editor"; import type { BridgeField, BridgeRow } from "../types/database-view.types"; import styles from "./table-renderer.module.css"; /** * NOTE: This renderer uses plain HTML table elements. * * TanStack Table v8 (@tanstack/react-table) is NOT yet installed in * apps/client/package.json. The headless table logic here is a faithful * placeholder that mirrors the TanStack Table v8 mental model (columns derived * from BridgeField[], rows from BridgeRow[]) so migration is a drop-in. * * To migrate: install @tanstack/react-table@^8, then replace the manual * column/row loops with useReactTable() + flexRender(). The data shape * (fields + rows) is already correct. * * DEPENDENCY NEEDED: @tanstack/react-table@^8 */ const PAGE_SIZE = 50; interface TableRendererProps { tableId: string; viewId: string; bridgeUrl?: string | null; } interface EditingCell { rowId: string; fieldId: string; } /** Display format for a raw cell value — keeps the table readable without edit. */ function formatCellValue(value: unknown): string { if (value === null || value === undefined) return ""; if (typeof value === "boolean") return value ? "true" : "false"; if (typeof value === "object") { // Arrays of strings/objects (select, link fields in Baserow) if (Array.isArray(value)) { return value .map((v) => (typeof v === "object" && v !== null ? (v as { value?: string }).value ?? JSON.stringify(v) : String(v))) .join(", "); } // Objects (file fields, etc.) return (value as { value?: string }).value ?? JSON.stringify(value); } return String(value); } /** Loading skeleton mimicking the table layout. */ function TableSkeleton() { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); } interface TableBodyProps { fields: BridgeField[]; rows: BridgeRow[]; editingCell: EditingCell | null; canWrite: boolean; onCellDoubleClick: (rowId: string, fieldId: string) => void; onCellSave: (rowId: string, field: BridgeField, value: unknown) => void; onCellCancel: () => void; } function TableBody({ fields, rows, editingCell, canWrite, onCellDoubleClick, onCellSave, onCellCancel, }: TableBodyProps) { const { t } = useTranslation(); if (rows.length === 0) { return ( {t("database_view.table.empty_state")} ); } return ( <> {rows.map((row) => ( {fields.map((field) => { const isEditing = editingCell?.rowId === row.id && editingCell?.fieldId === field.id; const cellValue = row.fields[field.name] ?? row.fields[field.id]; return ( { if (!isEditing) { onCellDoubleClick(row.id, field.id); } }} > {isEditing ? ( onCellSave(row.id, field, v)} onCancel={onCellCancel} /> ) : ( formatCellValue(cellValue) )} ); })} ))} ); } export function TableRenderer({ tableId, viewId, bridgeUrl }: TableRendererProps) { const { t } = useTranslation(); const [page, setPage] = useState(1); const [editingCell, setEditingCell] = useState(null); const { data, isLoading, isError, error, refetch } = useViewData({ viewId, bridgeUrl, page, size: PAGE_SIZE, }); const { canWriteRows } = usePermissions(); const updateRow = useUpdateRow({ tableId, viewId, bridgeUrl }); // Subscribe to SSE updates — invalidates React Query cache on row/view events. useDatabaseRealtimeUpdates(tableId, viewId, bridgeUrl); if (isLoading) { return ; } if (isError) { const axiosError = error as { response?: { status?: number } }; const status = axiosError?.response?.status; let message = t("database_view.error.generic"); if (status === 403) message = t("database_view.error.permission_denied"); else if (status === 404) message = t("database_view.error.view_not_found"); return ( } color="red" title={t("database_view.error.title")} > {message} ); } const { rows, fields, total, hasNextPage } = data ?? { rows: [], fields: [], total: 0, hasNextPage: false, }; function handleCellSave(rowId: string, field: BridgeField, value: unknown) { setEditingCell(null); updateRow.mutate({ rowId, payload: { fields: { [field.name]: value } }, }); } return (
{fields.map((field) => ( ))} setEditingCell({ rowId, fieldId }) } onCellSave={handleCellSave} onCellCancel={() => setEditingCell(null)} />
{field.name}
{/* Pagination — only shown when there is more than one page. */} {(page > 1 || hasNextPage) && (
{t("database_view.table.page_info", { page, total, size: PAGE_SIZE, })}
)}
); }