Sprinter Docs

Data Table

Generic, reusable data table component with inline editing, keyboard navigation, virtualization, and domain adapters

Overview

The Data Table module (features/data-table/) provides a generic DataTable<T> component built on TanStack Table v8. It is fully decoupled from any domain — entity-specific, task-specific, or any other data shape is mapped through adapter components that translate domain schemas into the generic ColumnSchema<T> interface.

The old entity-coupled data table (~1,089 lines in a single file with 31 supporting files) was replaced by this modular architecture.

Key Concepts

ColumnSchema<T>

The core type that defines a column. Each column declares its id, header, accessorFn, semantic type (string, number, boolean, enum, date, rich-text, array, relation, currency, percentage, status, custom), and optional features like editable, hidden, pinnable, renderCell, and renderEditor.

Cell Modes

Cells operate in two modes via the InteractionMask pattern:

  • SELECT — navigate with arrow keys, Enter/F2 to edit, Delete to clear
  • EDIT — inline editor receives focus, Escape cancels, Enter/Tab commits

Domain Adapters

Adapters bridge domain data to the generic table:

  • EntityDataTable — maps EntityTypeRecord.json_schemaColumnSchema<EntityRecord>[], wires inline edits to PATCH /api/entities/[id]
  • TaskDataTable — maps task fields → ColumnSchema<TaskRecord>[]
  • DataTableBlock — renders EntityDataTable inside a view block with config overrides

Architecture

features/data-table/
├── core/                 # Types, constants, context, build-column-defs, components
│   ├── types.ts          # ColumnSchema<T>, DataTableProps<T>, FilterState, etc.
│   ├── constants.ts      # Density classes, page sizes, thresholds
│   ├── context.tsx       # DataTableProvider — shared state for sub-components
│   ├── build-column-defs.tsx  # ColumnSchema[] → TanStack ColumnDef[]
│   ├── data-table.tsx    # Main orchestrator component
│   ├── data-table-cell.tsx   # Cell renderer (display + edit mode)
│   ├── data-table-head.tsx   # Header row with sort indicators
│   ├── data-table-body.tsx   # Body with standard + virtualized rendering
│   ├── data-table-row.tsx    # Single row component
│   └── data-table-footer.tsx # Summary footer row
├── editors/              # Inline editor registry + 6 editor types
│   ├── editor-registry.ts    # Type → editor component lookup
│   ├── text-editor.tsx
│   ├── number-editor.tsx
│   ├── boolean-editor.tsx
│   ├── enum-editor.tsx
│   ├── date-editor.tsx
│   └── rich-text-editor.tsx  # Tiptap via lazy next/dynamic
├── features/             # Toolbar features
│   ├── toolbar.tsx           # Search, density, column visibility
│   ├── pagination.tsx        # Page navigation + size selector
│   ├── bulk-actions.tsx      # Selection-based actions bar
│   ├── column-pinning.tsx    # Pin/unpin column toggles
│   ├── column-visibility.tsx # Show/hide column checkboxes
│   └── density-toggle.tsx    # Compact/default/spacious
├── interaction/          # Hooks for cell-level interaction
│   ├── use-cell-navigation.ts   # Arrow keys, Tab, Enter/Escape
│   ├── use-cell-editing.ts      # Edit state machine + type coercion
│   ├── use-cell-clipboard.ts    # Ctrl+C/V copy-paste
│   ├── use-column-resize.ts     # CSS-variable resize + persistence
│   └── use-row-selection.ts     # Checkbox + shift-click selection
└── virtualization/       # Large dataset support
    ├── use-virtual-rows.ts      # @tanstack/react-virtual integration
    └── focus-sink.tsx           # Hidden input for keyboard capture

How It Works

Data Flow

  1. Adapter builds ColumnSchema<T>[] from domain schema
  2. buildColumnDefs converts to TanStack ColumnDef[], adding select column
  3. useReactTable manages sort, filter, pagination, visibility, pinning state
  4. Context distributes table instance + interaction state to sub-components
  5. Cell renderer looks up ColumnSchema by column ID, renders display or editor
  6. On commituseCellEditing.commitEdit coerces value → calls onCellEdit → adapter persists

Pagination

Two modes:

  • Client-side (default) — getPaginationRowModel() handles paging within loaded data
  • Server-side — activated when onPageChange + total are provided; uses manualPagination: true and delegates page fetching to the consumer

Hidden Columns

Columns with hidden: true in their schema are included in the TanStack model but start with visibility: false. Users can re-enable them via the column visibility toggle in the toolbar.

API Reference

DataTable<TData>

Main component. Key props:

PropTypeDescription
dataTData[]Rows to render
columnsColumnSchema<TData>[]Column definitions
getRowId(row: TData) => stringStable row ID extractor
totalnumberTotal rows across all pages
onCellEdit(rowId, colId, value) => Promise<void>Inline edit persistence
onPageChange(page: number) => voidServer-side page navigation
configDataTableConfigFeature toggles and display options
bulkActionsBulkAction<TData>[]Actions for selected rows

ColumnSchema<TData>

FieldTypeDefaultDescription
idstringrequiredUnique column identifier
headerstringrequiredDisplay label
accessorFn(row: TData) => unknownrequiredValue extractor
typeColumnTyperequiredSemantic type for rendering/editing
editablebooleanfalseAllow inline editing
hiddenbooleanfalseStart hidden, user can reveal
renderCell(value, row) => ReactNodeCustom display renderer
renderEditor(props) => ReactNodeCustom editor component

For Agents

Agents interact with data tables indirectly through entity/task CRUD tools. The data table is a presentation component — agents create/update entities via createEntity, updateEntity, and the table reflects those changes on refresh.

Design Decisions

  • Generic over TData — React context can't be generic, so the context uses any for the table instance and column schemas. Type safety is enforced at the adapter boundary.
  • InteractionMask pattern — a single hidden <FocusSink> input captures all keyboard events, avoiding per-cell focus management complexity.
  • CSS-variable column resize — widths are computed once per resize event and consumed via calc(var(--col-width)), avoiding per-cell style recalculation.
  • Editor registry — editors are looked up by ColumnType at render time via getEditorForType(), making it trivial to add new editor types.
  • Domain adapters over configuration — instead of a mega-config object, each domain creates a thin adapter component that maps its schema to ColumnSchema[] and wires domain-specific callbacks.

On this page