Skip to content

feat(ui): standardised datatables (sort, sticky header, column picker, selection) #138

Description

@pskillen

Problem

The shipped DataTable from #105 is a thin Mantine Table wrapper: striped rows, optional toolbar slot, no column sorting, no sticky header, no row selection, and no built-in search/sort/column-picker affordances.

List behaviour is inconsistent and external to the table:

List route Filtering / sort Column visibility
Channels Band, mode, name, distance filters + sort modes in ChannelsListSectionNav via useChannelListQuery Optional columns in section nav via useChannelListColumns + localStorage
Zones, Contacts, Talk groups, RX group lists Name filter only (useListNameQuery + ?q=) applied in the route before passing rows Fixed columns — no picker
Detail pages Embedded DataTable for reference lists (members, usage counts) — read-only

Operators get a functional spreadsheet view but cannot sort by clicking headers, scroll long lists without losing column context, or use a consistent filter/column toolbar across entity lists. Channel-specific logic will not scale as lists grow (more optional columns, bulk actions, export selections).

Intended outcome

Replace the naive table with a standardised list datatable aligned with Mantine UI table patterns (sticky header, sortable columns, search, optional multi-select — adopt other recipes from that category where useful).

Core DataTable capabilities

Feature Notes
Sticky header Header row stays visible inside scroll area on long lists
Column sort Click header to sort asc/desc; support custom comparators per column (e.g. MHz numeric, name locale, distance when present)
Search / filter slot Built-in toolbar region: global text search or wired external filter state (channels keep richer filters in section nav; simpler lists get in-table search)
Column show/hide Standardised optional-column picker (persist per list in localStorage); generalise channel-only CHANNEL_OPTIONAL_COLUMNS pattern
Row selection (optional) Multi-select checkbox column for future bulk actions; off by default on read-only embedded tables
Empty state Keep existing EmptyState; respect filtered-empty vs truly-empty copy
Mobile Implement or wire mobileColumnPolicy — coordinate with #68 (collapse name/band/mode, merge RX/TX)

API shape (sketch — refine in PR)

Evolve DataTableColumn to carry sortability, default visibility, and comparator:

interface DataTableColumn<T> {
  key: string;
  header: string;
  render: (row: T) => ReactNode;
  sortable?: boolean;
  sortValue?: (row: T) => string | number | null;
  defaultVisible?: boolean;
  hideable?: boolean;
}

Toolbar composition: DataTable owns layout (search input, column multi-select, result count); routes/section nav supply filter state via props or render props rather than reimplementing controls ad hoc.

Implementation options (pick in PR — not prescriptive):

Either way: one table abstraction in src/components/ui/; detail-page embedded tables use the same primitive with selectable={false} and fewer toolbar features.

Adoption — entity list pages

Migrate primary list routes to the new table + toolbar contract:

Follow-up (optional): UkRepeaterSearch uses raw Mantine Table — adopt kit table if selection/sort patterns align.

Affected

  • src/components/ui/DataTable.tsx (+ tests)
  • src/hooks/useChannelListColumns.ts, channelListQueryUtils.ts — generalise or delegate to table
  • src/components/SectionNav/sections/ChannelsListSectionNav.tsx — dedupe column picker / search if moved into table toolbar
  • src/hooks/useListNameQuery.ts — may merge into shared list-query hook or table-controlled search
  • docs/features/ui/README.md — document datatable contract

Related

Out of scope (initial slice)

  • Server-side pagination (all data is in-memory LocalStorage today)
  • Bulk delete/edit actions (selection column is groundwork only unless a small win is trivial)
  • Virtualised rows for 10k+ channels (measure first; add follow-up if needed)
  • Replacing BandPlanTable / non-entity reference tables

Manual verify

  1. Each entity list: click column headers to sort; order toggles asc/desc; numeric columns sort numerically.
  2. Sticky header remains visible when scrolling 50+ rows.
  3. Channels: optional columns toggle from table toolbar; choice persists across reload.
  4. Contacts/zones/etc.: name search works via table toolbar or existing ?q= (document chosen behaviour).
  5. Styleguide demos sort + selection + column picker.
  6. Mobile / narrow viewport: #68 collapse behaviour or acceptable horizontal scroll documented.
  7. npm run lint && npm run test && npm run build.

Workflow note

Phased PR(s) acceptable — e.g. (1) core DataTable + styleguide, (2) migrate simple lists, (3) channels + section-nav dedupe + #68 mobile. Branch from origin/main, atomic conventional commits per slice, link Closes #.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions