From b31109298704b681c1cf04466170b5d55cb3806d Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sun, 5 Apr 2026 17:57:27 -0400 Subject: [PATCH] Records page: sortable headers and short date formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Click any column header to sort asc/desc (⇅ / ▲ / ▼ indicators) - Sort is client-side within the current page, numeric-aware - Dates matching ISO format are displayed as e.g. "Apr 5, 26" - Sort resets on source change Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/Records.jsx | 68 ++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/ui/src/pages/Records.jsx b/ui/src/pages/Records.jsx index e7cefad..b33ca55 100644 --- a/ui/src/pages/Records.jsx +++ b/ui/src/pages/Records.jsx @@ -1,16 +1,30 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { api } from '../api' +const DATE_RE = /^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/ + +function formatVal(val) { + if (val === null || val === undefined) return null + const s = String(val) + if (DATE_RE.test(s)) { + const d = new Date(s) + if (!isNaN(d)) return d.toLocaleDateString(undefined, { year: '2-digit', month: 'short', day: 'numeric' }) + } + return s +} + export default function Records({ source }) { const [rows, setRows] = useState([]) const [exists, setExists] = useState(null) const [offset, setOffset] = useState(0) const [loading, setLoading] = useState(false) + const [sort, setSort] = useState({ col: null, dir: 'asc' }) const LIMIT = 100 useEffect(() => { if (!source) return setOffset(0) + setSort({ col: null, dir: 'asc' }) load(0) }, [source]) @@ -27,11 +41,30 @@ export default function Records({ source }) { } } + function toggleSort(col) { + setSort(s => s.col === col + ? { col, dir: s.dir === 'asc' ? 'desc' : 'asc' } + : { col, dir: 'asc' } + ) + } + + const sorted = useMemo(() => { + if (!sort.col) return rows + return [...rows].sort((a, b) => { + const av = a[sort.col] ?? '' + const bv = b[sort.col] ?? '' + const cmp = String(av).localeCompare(String(bv), undefined, { numeric: true }) + return sort.dir === 'asc' ? cmp : -cmp + }) + }, [rows, sort]) + function prev() { const o = Math.max(0, offset - LIMIT); setOffset(o); load(o) } function next() { const o = offset + LIMIT; setOffset(o); load(o) } if (!source) return
Select a source first.
+ const cols = rows.length > 0 ? Object.keys(rows[0]) : [] + return (
@@ -59,19 +92,34 @@ export default function Records({ source }) { - {Object.keys(rows[0]).map(col => ( - - ))} + {cols.map(col => { + const active = sort.col === col + return ( + + ) + })} - {rows.map((row, i) => ( + {sorted.map((row, i) => ( - {Object.values(row).map((val, j) => ( - - ))} + {cols.map((col, j) => { + const formatted = formatVal(row[col]) + return ( + + ) + })} ))}
{col} toggleSort(col)} + className="px-3 py-2 font-medium whitespace-nowrap cursor-pointer select-none hover:text-gray-600" + > + {col} + + {active ? (sort.dir === 'asc' ? '▲' : '▼') : '⇅'} + +
- {val === null ? : String(val)} - + {formatted === null ? : formatted} +