// CIL[BEGIN chunk_id="RecordListPage_module_header_1784761401" type="module_header" identity="module_header" parent="none"] // CIL[DOC-MACRO purpose="Render a sortable/filterable table of records for a Workbench type"] // CIL[DOC-RESPONSIBILITY responsibility="Fetch records via admin-data and display them in a table with row navigation"] // CIL[DOC-CONTEXT module="workbench.components.records.RecordListPage" layer="frontend-component" related_files="[\"custom/apps/workbench/components/records/RecordDetailPage.tsx\",\"custom/apps/workbench/hooks/useWorkbenchList.ts\"]"] // CIL[DOC-CONTRACT inputs="typeSlug:string, title:string, columns:Column[], getDetailRoute:(record)=>string, createRoute?:string" outputs="JSX list page" errors="fetch errors rendered inline" side_effects="calls /api/admin-data, navigates on row click"] // CIL[DOC-MICRO logic_summary="Fetches up to 500 records, offers a text filter, and renders a shadcn table with clickable rows" notable_decisions="[]"] // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] import { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { apiFetch } from '@core/lib/api' import { Button } from '@core/components/ui/button' import { Input } from '@core/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@core/components/ui/table' import { Plus, Search } from 'lucide-react' // CIL[END chunk_id="RecordListPage_module_header_1784761401"] // CIL[BEGIN chunk_id="RecordListPage_types_1784761401" type="type" identity="Column" parent="none"] // CIL[DOC-MACRO description="Implementation of Column" macro="Renders and manages Column UI state" input_spec="record object" output_spec="React node"] // CIL[DOC-CONTRACT input_spec="See module header or type signature" output_spec="JSX or value" side_effects="none"] // CIL[DOC-STABILITY stability="experimental"] export interface Column { key: string label: string render?: (record: any) => React.ReactNode } // CIL[END chunk_id="RecordListPage_types_1784761401"] // CIL[BEGIN chunk_id="RecordListPage_recordListPage_1784761401" type="function" identity="RecordListPage" parent="none"] // CIL[DOC-MACRO description="Implementation of RecordListPage" macro="Renders and manages RecordListPage UI state"] // CIL[DOC-CONTRACT input_spec="See module header or type signature" output_spec="JSX or value" side_effects="none"] // CIL[DOC-STABILITY stability="experimental"] export interface RecordListPageProps { typeSlug: string title: string columns: Column[] getDetailRoute: (record: any) => string createRoute?: string emptyMessage?: string extraParams?: Record filterFn?: (record: any) => boolean } export function RecordListPage({ typeSlug, title, columns, getDetailRoute, createRoute, emptyMessage = 'No records found.', extraParams = {}, filterFn, }: RecordListPageProps) { const navigate = useNavigate() const [records, setRecords] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [filter, setFilter] = useState('') const [sort, setSort] = useState<{ key: string; direction: 'asc' | 'desc' }>({ key: 'created_at', direction: 'desc' }) const load = async () => { setLoading(true) setError(null) try { const params = new URLSearchParams({ entity: 'items', type_slug: typeSlug, limit: '500', ...extraParams }) const res = await apiFetch(`/api/admin-data?${params.toString()}`) const json = await res.json() const data = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [] setRecords(data) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load records') } finally { setLoading(false) } } useEffect(() => { load() }, [typeSlug]) const filtered = useMemo(() => { const term = filter.trim().toLowerCase() let rows = records if (filterFn) { rows = rows.filter(filterFn) } if (term) { rows = rows.filter(r => [r.title, r.description, r.status, r.id] .concat(Object.values(r.data || {})) .some(v => String(v || '').toLowerCase().includes(term)) ) } rows = [...rows].sort((a, b) => { const av = a[sort.key] ?? a.data?.[sort.key] ?? '' const bv = b[sort.key] ?? b.data?.[sort.key] ?? '' if (av < bv) return sort.direction === 'asc' ? -1 : 1 if (av > bv) return sort.direction === 'asc' ? 1 : -1 return 0 }) return rows }, [records, filter, sort]) const handleSort = (key: string) => { setSort(prev => ({ key, direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc', })) } return (

{title}

setFilter(e.target.value)} placeholder="Filter records…" className="pl-9 w-64" />
{createRoute && ( )}
{loading ? (
Loading {title.toLowerCase()}…
) : error ? (
{error}
) : filtered.length === 0 ? (
{emptyMessage}
) : ( {columns.map(col => ( handleSort(col.key)}> {col.label} ))} {filtered.map(record => ( navigate(getDetailRoute(record))} > {columns.map(col => ( {col.render ? col.render(record) : String(record[col.key] ?? record.data?.[col.key] ?? '—')} ))} ))}
)}
) } // CIL[END chunk_id="RecordListPage_recordListPage_1784761401"]