Spec: F66 — TanStack Table (Declarative Data Tables)
Status: partial (source-backed; Tasks/Contacts migrations and runtime evidence remain open)
1. Problem & CEO Job
Decide fast. CEO-facing pages are data lists, but each hand-rolls sorting, filtering, pagination, and selection. The result is inconsistent behavior, missed edge cases, and slow delivery. A headless table library removes that boilerplate.
2. User Stories & Acceptance Criteria
-
As a CEO, when I view any data table, I want to sort by clicking column headers, so that I find what I need quickly.
- Given a table with sortable columns, clicking a header sorts ascending, clicking again sorts descending (DataTable.tsx handleSort)
- Given an active sort, the header shows a sort indicator (arrow icon) (ChevronUp/Down icons, lines 146–149)
-
As a CEO, when I view a table with many rows, I want client-side pagination, so that I see 25 rows at a time without scrolling endlessly.
- Given >25 items, the table shows pagination controls (DataTable.tsx pagination bar, line 283)
- Given paginated data, clicking “Next” shows the next page (page state + paged slice)
-
As a CEO, when I check off multiple items, I want row selection with bulk actions, so that I can act on several items at once.
- Given a table with checkboxes, selecting N rows reveals a bulk action bar (DataTable
selectableprop; consumers wire bulk bar — e.g., DealsPage BulkActionBar) - Given selected rows, clicking “Xóa” deletes all selected (consumer-provided action via onSelectionChange + external bulk handler)
- Given a table with checkboxes, selecting N rows reveals a bulk action bar (DataTable
-
As a developer, I want a single
<DataTable>component that accepts columns and data, so that a new list page is ~20 lines of markup.
3. Scope Boundaries
- In: Installation (
@tanstack/react-table), shared<DataTable>component with sort/filter/pagination/selection,<TableSkeleton>and<TableEmpty>states, migration of existing list pages one-by-one - Out: Server-side pagination/sorting (all current lists fetch fully client-side), inline editing, row reordering, column visibility toggles, export-to-CSV
4. Data Model
n/a — purely frontend.
5. API Contract
n/a — tables consume existing TanStack Query data.
6. UI / UX
- Component:
frontend/src/components/table/DataTable.tsx— wraps shadcn’s<Table>,<TableHeader>,<TableBody>,<TableRow>,<TableCell>with sort/select/pagination built in - Sorting:
<DataTable>acceptssortable: booleanper column (default true for text/date/number columns) - Selection:
<DataTable selection={true}>shows checkbox column, exposesselectedRowsvia render prop - Pagination:
<DataTable pagination={true} pageSize={25}>shows “Hiển thị 1–25 / 80” - Bulk actions: Pass
bulkActions={[{ label: 'Xóa', onClick: (rows) => ... }]}— renders as a floating action bar when rows selected - States:
- Loading:
<TableSkeleton />— N rows of<Skeleton>matching column count - Empty:
<TableEmpty message={t('empty')} />— centered text + optional CTA button - Error: Toast from TanStack Query
isError— table shows empty state with retry button - Populated: Full table with sort/select/pagination
- Loading:
- i18n:
commonnamespace keys —common.no_data,common.rows_per_page,common.of,common.selected,common.sort_asc,common.sort_desc,common.clear_sort - Mobile: Tables collapse to card list on <768px — each row becomes a
<Card>with label:value pairs.useMediaQueryfrom shadcn hooks to detect breakpoint.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | DataTable renders with data and columns | frontend | frontend/src/test/DataTable.test.tsx |
| 2 | DataTable sorts by column on header click | frontend | frontend/src/test/DataTable.test.tsx |
| 3 | DataTable paginates correctly | frontend | frontend/src/test/DataTable.test.tsx |
| 4 | DataTable row selection works | frontend | frontend/src/test/DataTable.test.tsx |
| 5 | DataTable shows skeleton on loading | frontend | frontend/src/test/DataTable.test.tsx |
| 6 | DataTable shows empty state | frontend | frontend/src/test/DataTable.test.tsx |
| 7 | DataTable collapses to cards on mobile | frontend | frontend/src/test/DataTable.test.tsx |
| 8 | Each migrated page — sorting works | e2e | Existing describe blocks |
| 9 | Deals table view renders on /deals after Table toggle | e2e | frontend/tests/e2e/crm.spec.ts — F66 — TanStack Table (DataTable) |
| 10 | Column headers exist and sort click leaves table visible | e2e | frontend/tests/e2e/crm.spec.ts — deals table view renders sortable columns |
| 11 | Table has more than 2 column headers | e2e | frontend/tests/e2e/crm.spec.ts — deals table view has multiple column headers |
8. Current Behavior
The shared <DataTable> primitive uses TanStack Table v8 and is reused by CRM, project-task, commitment, subscription, HQ, and settings surfaces. The top-level Tasks and Contacts list surfaces still use custom list/virtual-list UIs rather than the shared table. Browser and frontend runner evidence remain open.
9. Error States
- Fetch error: TanStack Query error → table shows
<TableEmpty>with “Không thể tải dữ liệu” + retry button - Empty data:
<TableEmpty>with “Chưa có dữ liệu” + CTA to create - Selection + bulk action error: Toast “Không thể thực hiện” + selection persists so user can retry
10. Diagrams & Docs Touched
TASTE.md— add table rule (use DataTable, don’t hand-roll)frontend/AGENTS.md— add DataTable convention section
11. Implementation Audit
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | TanStack Table v8 installed | ✅ match | package.json |
| 2 | frontend/src/pages/crm/TableView.tsx — sortable, column-togglable deals table with DealHealthBadge | ✅ match | TableView.tsx |
| 3 | Shared <DataTable> wrapper component established | ✅ source match | frontend/src/components/table/DataTable.tsx; tests in frontend/src/test/_shared/DataTable.test.tsx |
| 4 | Tasks + contacts table surfaces migrated | ⚠️ gap | TasksPage.tsx still uses the drag/drop card list and ContactsPage.tsx still uses VirtualList; shared rollout elsewhere is source-backed |
2026-07-16 — Contract Graph audit
The shared table primitive and current rollout are now contract-owned. Tasks and Contacts remain explicit implementation gaps; no browser or production evidence is inferred.
12. Open Questions / Decisions
- TanStack Table v8 (headless) vs AG Grid (batteries-included)? Decision: TanStack Table v8. AG Grid adds 500KB+ and its configuration model fights React patterns. TanStack Table is headless, zero-bundle visual, and pairs naturally with shadcn
Tablecomponents.
13. Bug / Change Log
2026-07-01 — Correct F66 rollout truth without over-claiming the remaining gap
Type: Documentation truth fix
Summary:
- Updated F66 to reflect the real shipped
DataTablerollout across the repo instead of implying CRM was the only live consumer. - Kept the genuine remaining gap open: the top-level Tasks and Contacts pages still have not migrated to the shared table surface.
2026-06-13 — E2E tests added for deals table view (T3-E r11)
Type: Test
Summary:
- Added
F66 — TanStack Table (DataTable)E2E describe block infrontend/tests/e2e/crm.spec.ts. - Tests: table view renders after toggle, sortable column headers visible, multiple column count assertion.
- Unskipped
can switch to table viewtest in F50 block.
Impact:
- E2E gate for F66 DataTable now active in CI.