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— mapsEntityTypeRecord.json_schema→ColumnSchema<EntityRecord>[], wires inline edits toPATCH /api/entities/[id]TaskDataTable— maps task fields →ColumnSchema<TaskRecord>[]DataTableBlock— rendersEntityDataTableinside 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 captureHow It Works
Data Flow
- Adapter builds
ColumnSchema<T>[]from domain schema buildColumnDefsconverts to TanStackColumnDef[], adding select columnuseReactTablemanages sort, filter, pagination, visibility, pinning state- Context distributes table instance + interaction state to sub-components
- Cell renderer looks up
ColumnSchemaby column ID, renders display or editor - On commit —
useCellEditing.commitEditcoerces value → callsonCellEdit→ adapter persists
Pagination
Two modes:
- Client-side (default) —
getPaginationRowModel()handles paging within loaded data - Server-side — activated when
onPageChange+totalare provided; usesmanualPagination: trueand 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:
| Prop | Type | Description |
|---|---|---|
data | TData[] | Rows to render |
columns | ColumnSchema<TData>[] | Column definitions |
getRowId | (row: TData) => string | Stable row ID extractor |
total | number | Total rows across all pages |
onCellEdit | (rowId, colId, value) => Promise<void> | Inline edit persistence |
onPageChange | (page: number) => void | Server-side page navigation |
config | DataTableConfig | Feature toggles and display options |
bulkActions | BulkAction<TData>[] | Actions for selected rows |
ColumnSchema<TData>
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique column identifier |
header | string | required | Display label |
accessorFn | (row: TData) => unknown | required | Value extractor |
type | ColumnType | required | Semantic type for rendering/editing |
editable | boolean | false | Allow inline editing |
hidden | boolean | false | Start hidden, user can reveal |
renderCell | (value, row) => ReactNode | — | Custom display renderer |
renderEditor | (props) => ReactNode | — | Custom 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
anyfor 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
ColumnTypeat render time viagetEditorForType(), 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.
Related Modules
- Entity System — entity adapter source
- Block System — data table block rendering
- View System — view configs that include data table blocks
- Tasks — task adapter source
Block System
The universal rendering primitive. Every visual surface in the platform -- entity details, dashboards, chat messages, list views -- renders through the same BlockConfig to ResolvedBlock to component pipeline.
Interactivity
Four canonical shapes — Call, Form, View, Link — that unify tool calls, structured input, surface projections, and external navigation across Amble and MCP.