From c75ccfe11c4b8e7742a4daebbc264703b86e0714 Mon Sep 17 00:00:00 2001 From: MMeteorL Date: Thu, 28 May 2026 00:22:22 -0700 Subject: [PATCH 1/2] Add column-click sorting to dataset table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a column header cycles through ascending → descending → unsorted. The sort is client-side: TanStack Table's getSortedRowModel reorders the rows array that react-window renders, so no extra Convex queries are needed. Implementation: - DatasetTable: adds SortingState + getSortedRowModel; sets sortingFn "alphanumeric" on every data column so numeric strings (e.g. "42", "$1,234") sort numerically and text sorts case-insensitively. - ColumnHeader: makes the cell content area clickable via header.column.getToggleSortingHandler(); shows ▲/▼ when sorted and a faint ⇅ hint on hover when unsorted; sets aria-sort for accessibility. The resize-handle zone is unaffected (it fires onMouseDown, not onClick). Co-Authored-By: Claude Sonnet 4.6 --- frontend/components/table/ColumnHeader.tsx | 65 +++++++++++++++++++++- frontend/components/table/DatasetTable.tsx | 10 ++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/frontend/components/table/ColumnHeader.tsx b/frontend/components/table/ColumnHeader.tsx index 019905c..cc9a5e6 100644 --- a/frontend/components/table/ColumnHeader.tsx +++ b/frontend/components/table/ColumnHeader.tsx @@ -5,6 +5,51 @@ import type { DatasetRow, DatasetColumn } from "./types"; import { ColumnIcon } from "./ColumnIcon"; import { floorWidth } from "./utils"; +function SortIndicator({ direction }: { direction: false | "asc" | "desc" }) { + if (direction === "asc") { + return ( + + ); + } + if (direction === "desc") { + return ( + + ); + } + // Unsorted: show a faint up/down chevron pair as a hint that the column is sortable + return ( + + ); +} + export function ColumnHeader({ header, column, @@ -16,6 +61,10 @@ export function ColumnHeader({ isResizing: boolean; containerHeight: number; }) { + const isSorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + const toggleSort = header.column.getToggleSortingHandler(); + return (
{column && } {column?.isPrimaryKey && ( @@ -45,6 +107,7 @@ export function ColumnHeader({ )} {column?.name ?? header.id} + {canSort && }
observer.disconnect(); }, []); + const [sorting, setSorting] = useState([]); + const [storedWidths, setStoredWidths] = usePersistedColumnWidths(datasetId); const columns = useMemo( @@ -96,6 +103,9 @@ export function DatasetTable({ columns, columnResizeMode: "onChange", getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + state: { sorting }, getRowId: (row) => row._id, }); From c4c07df51369c71bc0404e66c85db0f45cc19289 Mon Sep 17 00:00:00 2001 From: MMeteorL Date: Fri, 29 May 2026 16:50:03 -0700 Subject: [PATCH 2/2] fix: address CodeRabbit review comments on column sorting - ColumnHeader: add tabIndex and onKeyDown (Enter/Space) so sortable column headers are keyboard-accessible, matching role="button" semantics - DatasetTable: replace sortingFn "alphanumeric" with a custom comparator that strips currency/thousands formatting and compares numerically, falling back to locale-insensitive string comparison for non-numeric values Co-Authored-By: Claude Sonnet 4.6 --- frontend/components/table/ColumnHeader.tsx | 11 +++++++++++ frontend/components/table/DatasetTable.tsx | 23 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/frontend/components/table/ColumnHeader.tsx b/frontend/components/table/ColumnHeader.tsx index cc9a5e6..4ba9361 100644 --- a/frontend/components/table/ColumnHeader.tsx +++ b/frontend/components/table/ColumnHeader.tsx @@ -88,6 +88,17 @@ export function ColumnHeader({ style={{ padding: "var(--table-cell-py) var(--table-cell-px)" }} onClick={canSort ? toggleSort : undefined} role={canSort ? "button" : undefined} + tabIndex={canSort ? 0 : undefined} + onKeyDown={ + canSort + ? (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + header.column.toggleSorting(); + } + } + : undefined + } aria-sort={ isSorted === "asc" ? "ascending" diff --git a/frontend/components/table/DatasetTable.tsx b/frontend/components/table/DatasetTable.tsx index 1d3ecd7..b1c1fce 100644 --- a/frontend/components/table/DatasetTable.tsx +++ b/frontend/components/table/DatasetTable.tsx @@ -45,9 +45,26 @@ function buildColumns( header: col.name, size: storedWidths[col.name] ?? DEFAULT_COL_WIDTH, minSize: MIN_COL_WIDTH, - // alphanumeric handles both plain strings and numeric values stored as - // strings (e.g. "42", "$1,234") and does case-insensitive comparison. - sortingFn: "alphanumeric", + // Custom sort: strip currency/thousands formatting then compare numerically; + // fall back to case-insensitive locale comparison for non-numeric values. + // TanStack's built-in "alphanumeric" sorts digit chunks individually so + // "$1,234" and "1234.56" don't sort correctly as numbers. + sortingFn: (rowA, rowB, columnId) => { + const a = rowA.getValue(columnId); + const b = rowB.getValue(columnId); + const toNum = (v: unknown): number => { + if (typeof v === "number") return v; + if (typeof v !== "string") return Number.NaN; + const n = Number(v.replace(/[^0-9.-]/g, "")); + return Number.isFinite(n) ? n : Number.NaN; + }; + const na = toNum(a); + const nb = toNum(b); + if (!Number.isNaN(na) && !Number.isNaN(nb)) return na - nb; + return String(a ?? "").localeCompare(String(b ?? ""), undefined, { + sensitivity: "base", + }); + }, }), );