import { useEffect, useState } from 'react' import { AssetStateBadge, Avatar, Button, EmployeeStatusBadge, EmptyState, HistoryActionBadge, Icon, SearchInput, Toolbar, } from '../ui' import { isDateType, isNumericType, toISODate } from '../../api/recordView' import { displayLabel, formatDate } from '../../utils/format' import { FileLinks, imageSources } from './FieldValue' /** * Renders a record view straight from the backend response: the columns are * whatever `config.fields` returns, in that order, with the backend's own * labels. Paging, sorting and search are handled by the server. * `toolbar` adds controls (e.g. a filter) next to the search box. A field with * `sortable: false` can't be sorted. A rich column (see api/richColumns.js) * shows several fields in one cell and sorts by its `sort_key`. */ export default function RecordView({ fields, rows, pagination, loading, error, query, setPage, setSearch, setSort, rowKey = 'instance_id', onRowClick, emptyText = 'No records yet.', toolbar, }) { const [term, setTerm] = useState(query.search) // Debounce typing so we don't fire a request per keystroke. useEffect(() => { if (term === query.search) return undefined const id = setTimeout(() => setSearch(term), 350) return () => clearTimeout(id) }, [term, query.search, setSearch]) // A rich column's fields stay searchable. const searchable = fields .flatMap((f) => (f.data_type === 'rich' ? [...f.title, ...f.subtitle] : [f])) .filter((f) => f.is_search) const columns = fields.filter((f) => f.field_key !== 'id') const sortKey = (field) => field.sort_key ?? field.field_key function toggleSort(field) { const dir = query.sortBy === sortKey(field) && query.sortDir === 'asc' ? 'desc' : 'asc' setSort(sortKey(field), dir) } const page = pagination?.page ?? 1 const totalPages = pagination?.total_pages ?? 1 const total = pagination?.total_count ?? rows.length return ( <> {searchable.length > 0 && ( displayLabel(f.output_label)).join(', ')}…`} /> )} {toolbar} {loading && Loading…} {error && (
{error}
)}
{columns.map((field) => { const label = displayLabel(field.output_label || field.field_key) if (field.sortable === false) return const sorted = query.sortBy === sortKey(field) return ( ) })} {rows.length === 0 ? ( ) : ( rows.map((row, i) => ( onRowClick(row) : undefined} > {columns.map((field) => ( ))} )) )}
{label} toggleSort(field)} > {label} {sorted ? (query.sortDir === 'asc' ? '▲' : '▼') : '⇅'}
{field.data_type === 'rich' ? renderRichCell(field, row) : renderCell(field, row[field.field_key])}
{totalPages > 1 && (
Page {page} of {totalPages}
)} ) } function cellClass(field) { if (field.data_type === 'id_gen') return 'cell-mono' if (isNumericType(field.data_type)) return 'cell-mono' return undefined } const present = (v) => v !== null && v !== undefined && v !== '' /** A rich column: its title fields in bold, its subtitle fields in a line below. */ function renderRichCell(field, row) { const title = field.title.filter((p) => present(row[p.field_key])) const subtitle = field.subtitle.filter((p) => present(row[p.field_key])) if (!title.length && !subtitle.length) return — const join = (parts, sep) => parts.map((p, i) => ( {i > 0 && sep} {p.prefix && `${p.prefix} `} {renderCell(p, row[p.field_key])} )) const name = title.map((p) => String(row[p.field_key])).join(' ') return (
{field.avatar && name && }
{title.length > 0 &&
{join(title, ' ')}
} {subtitle.length > 0 &&
{join(subtitle, ' · ')}
}
) } function renderCell(field, value) { if (value === null || value === undefined || value === '') { return — } if (field.data_type === 'image') { // Several photos (Front, Back…): show the first and how many more there are. const [first, ...more] = imageSources(value) if (!first) { // Uploaded file objects ({ blob_path, original_name… }) carry no URL to show. const count = Array.isArray(value) ? value.length : 0 return {count ? `${count} photo${count > 1 ? 's' : ''}` : '—'} } return ( {more.length > 0 && +{more.length}} ) } if (field.data_type === 'file') { return } if (isDateType(field.data_type)) { return formatDate(toISODate(value)) } if (field.data_type === 'email') { return {String(value)} } if (field.data_type === 'id_gen') { return {String(value)} } if (field.data_type === 'employee_status') { return } if (field.data_type === 'asset_state') { return } if (field.data_type === 'history_action') { return } if (typeof value === 'object') { return {JSON.stringify(value)} } return String(value) }