Files
calendar/packages/features/data-table/GUIDE.md
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
09ddbe886f refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier (#24513)
* refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier

- Remove Next.js usePathname dependency from DataTableProvider
- Make tableIdentifier a required prop instead of optional
- Update all usages to provide explicit tableIdentifier values
- This makes DataTableProvider usable in non-Next.js contexts

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: use usePathname at usage sites instead of hardcoding tableIdentifier

- Add validation in DataTableProvider for empty/nullish tableIdentifier
- Use usePathname() in Next.js apps to pass pathname as tableIdentifier
- Use descriptive identifiers for non-Next.js package components
- This keeps DataTableProvider framework-agnostic while allowing Next.js apps to use pathname

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* use pathname instead of hard-coded identifiers

* change type of tableIdentifier

* simplify

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-23 15:32:16 +02:00

1289 lines
34 KiB
Markdown

# DataTable System Guide
A comprehensive guide to using Cal.com's DataTable system for building powerful, filterable, and paginated data tables.
## Table of Contents
1. [Overview](#overview)
2. [Quick Start](#quick-start)
3. [Core Components](#core-components)
4. [Filter System](#filter-system)
5. [Segment System](#segment-system)
6. [Pagination Modes](#pagination-modes)
7. [Toolbar Components](#toolbar-components)
8. [Advanced Usage](#advanced-usage)
9. [Real-world Examples](#real-world-examples)
10. [TypeScript Types Reference](#typescript-types-reference)
11. [Best Practices](#best-practices)
## Overview
The DataTable system is a comprehensive solution for displaying tabular data with advanced features including:
- **Advanced filtering** with 5 filter types and custom operators
- **Segment system** for saving and sharing filter configurations
- **Traditional pagination** with page-based navigation (recommended)
- **Column management** (sorting, resizing, visibility)
- **Bulk actions** and selection management
- **Full TypeScript support**
- **Alternative infinite scroll** mode (has known issues, use with caution)
### Architecture
The system consists of three main layers:
1. **DataTableProvider** - Context provider managing all table state
2. **DataTableWrapper** - UI wrapper handling pagination, toolbars, and loading states
3. **DataTable** - Core table component with optional virtualization
#### Data Flow Diagram
```
┌─────────────────────────────────────────────────────────────────┐
│ DataTableProvider │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Table State │ │ Filter State │ │ Segment State │ │
│ │ • pagination │ │ • columnFilters │ │ • activeSegment │ │
│ │ • sorting │ │ • searchTerm │ │ • userSegments │ │
│ │ • columnVis │ │ • activeFilters │ │ • systemSegments│ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ DataTableWrapper │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Toolbar │ │ Data Table │ │ Pagination │ │
│ │ • SearchBar │ │ • Columns │ │ • PageControls │ │
│ │ • FilterBar │ │ • Rows │ │ • PageSize │ │
│ │ • Segments │ │ • Selection │ │ • TotalCount │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Backend API │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Filter Processing│ │ Data Fetching │ │ Response Format │ │
│ │ • makeWhereClause│ │ • Prisma Query │ │ • data[] │ │
│ │ • makeSqlCondition│ │ • Raw SQL │ │ • totalCount │ │
│ │ • Column Mapping │ │ • Pagination │ │ • meta │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
#### Component Hierarchy
```
DataTableProvider (Context)
└── DataTableWrapper (UI Container)
├── Toolbar
│ ├── ToolbarLeft
│ │ ├── DataTableToolbar.SearchBar
│ │ ├── DataTableFilters.FilterBar
│ │ └── DataTableFilters.ColumnVisibilityButton
│ └── ToolbarRight
│ ├── DataTableFilters.ClearFiltersButton
│ ├── DataTableSegment.SaveButton
│ └── DataTableSegment.Select
├── DataTable (Core Table)
│ ├── Table Header
│ ├── Table Body (Virtualized/Standard)
│ └── Selection Bar (Conditional)
└── Pagination Controls
├── Page Navigation
├── Page Size Selector
└── Total Count Display
```
## Quick Start
### Basic Setup
```tsx
import {
DataTableProvider,
DataTableWrapper,
DataTableFilters,
useDataTable,
ColumnFilterType,
} from "@calcom/features/data-table";
// 1. Define your data type
type User = {
id: number;
name: string;
email: string;
role: string;
};
// 2. Create columns with filtering support
const columns = [
{
id: "name",
header: "Name",
accessorKey: "name",
meta: {
type: ColumnFilterType.TEXT,
},
},
{
id: "role",
header: "Role",
accessorKey: "role",
meta: {
type: ColumnFilterType.SINGLE_SELECT,
},
},
];
// 3. Setup the table
function UserTable() {
const table = useReactTable({
data: users,
columns,
// ... other table options
});
const pathname = usePathname();
const tableIdentifier = "hard-coded idenfidier" // or pathname;
return (
<DataTableProvider tableIdentifier={tableIdentifier}>
<DataTableWrapper
table={table}
paginationMode="standard"
ToolbarLeft={
<>
<DataTableToolbar.SearchBar />
<DataTableFilters.FilterBar table={table} />
</>
}
ToolbarRight={
<>
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</>
}
/>
</DataTableProvider>
);
}
```
## Core Components
### DataTableProvider
The context provider that manages all table state including filters, sorting, pagination, and segments.
#### Props
```tsx
interface DataTableProviderProps {
tableIdentifier: string; // Unique identifier for the table (throws if empty)
children: React.ReactNode;
useSegments?: UseSegments; // Custom segment hook
defaultPageSize?: number; // Default: 10
ctaContainerClassName?: string; // CSS class for CTA container
segments?: FilterSegmentOutput[]; // Provided segments
timeZone?: string; // Timezone for date filters
preferredSegmentId?: SegmentIdentifier | null;
systemSegments?: SystemFilterSegment[];
}
```
#### Context Values
The provider exposes comprehensive state management:
```tsx
type DataTableContextType = {
// Filters
activeFilters: ActiveFilters;
addFilter: (columnId: string) => void;
updateFilter: (columnId: string, value: FilterValue) => void;
removeFilter: (columnId: string) => void;
clearAll: (exclude?: string[]) => void;
// Sorting
sorting: SortingState;
setSorting: OnChangeFn<SortingState>;
// Column management
columnVisibility: VisibilityState;
setColumnVisibility: OnChangeFn<VisibilityState>;
columnSizing: ColumnSizingState;
setColumnSizing: OnChangeFn<ColumnSizingState>;
// Pagination
pageIndex: number;
pageSize: number;
setPageIndex: (pageIndex: number | null) => void;
setPageSize: (pageSize: number | null) => void;
offset: number;
limit: number;
// Segments
segments: CombinedFilterSegment[];
selectedSegment: CombinedFilterSegment | undefined;
segmentId: SegmentIdentifier | null;
setSegmentId: (id: SegmentIdentifier | null) => void;
canSaveSegment: boolean;
isSegmentEnabled: boolean;
// Search
searchTerm: string;
setSearchTerm: (searchTerm: string | null) => void;
};
```
### DataTableWrapper
The main wrapper component that handles UI concerns, pagination, and toolbar layout.
#### Props
```tsx
type DataTableWrapperProps<TData> = {
table: ReactTableType<TData>;
testId?: string;
bodyTestId?: string;
isPending?: boolean;
totalRowCount?: number;
variant?: "default" | "compact";
className?: string;
containerClassName?: string;
headerClassName?: string;
rowClassName?: string;
children?: React.ReactNode;
tableContainerRef?: React.RefObject<HTMLDivElement>;
onRowMouseclick?: (row: Row<TData>) => void;
// Toolbar slots
ToolbarLeft?: React.ReactNode;
ToolbarRight?: React.ReactNode;
// Loading states
EmptyView?: React.ReactNode;
LoaderView?: React.ReactNode;
} & (
// Infinite pagination
| {
paginationMode: "infinite";
hasNextPage: boolean;
fetchNextPage: () => void;
isFetching: boolean;
}
// Standard pagination
| {
paginationMode: "standard";
hasNextPage?: never;
fetchNextPage?: never;
isFetching?: never;
}
);
```
### DataTable
The core table component with column resizing and pinning support.
#### Key Features
- **Column resizing** - Drag to resize columns
- **Column pinning** - Pin columns to left/right
- **Responsive design** - Adapts to mobile screens
- **Accessibility** - Full keyboard navigation support
- **Optional virtualization** - Available for infinite mode (use with caution)
## Filter System
The DataTable supports 5 filter types with various operators and options.
### Filter Types
#### 1. Single Select Filter
```tsx
{
type: ColumnFilterType.SINGLE_SELECT,
options: [
{ label: "Admin", value: "admin" },
{ label: "User", value: "user" },
{ label: "Guest", value: "guest" },
]
}
```
#### 2. Multi Select Filter
```tsx
{
type: ColumnFilterType.MULTI_SELECT,
options: [
{ label: "Engineering", value: "eng", section: "Departments" },
{ label: "Marketing", value: "marketing", section: "Departments" },
{ label: "Sales", value: "sales", section: "Departments" },
]
}
```
#### 3. Text Filter
```tsx
{
type: ColumnFilterType.TEXT,
textOptions: {
allowedOperators: ["contains", "equals", "startsWith"],
placeholder: "Search names..."
}
}
```
**Available operators:**
- `equals` - Exact match
- `notEquals` - Not equal
- `contains` - Contains substring
- `notContains` - Does not contain
- `startsWith` - Starts with
- `endsWith` - Ends with
- `isEmpty` - Is empty
- `isNotEmpty` - Is not empty
#### 4. Number Filter
```tsx
{
type: ColumnFilterType.NUMBER
}
```
**Available operators:**
- `eq` - Equal to
- `neq` - Not equal to
- `gt` - Greater than
- `gte` - Greater than or equal
- `lt` - Less than
- `lte` - Less than or equal
#### 5. Date Range Filter
```tsx
{
type: ColumnFilterType.DATE_RANGE,
dateRangeOptions: {
range: "past", // or "custom"
convertToTimeZone: true
}
}
```
### Column Filter Configuration
Add filtering to columns using the `meta` property:
```tsx
const columns = [
{
id: "status",
header: "Status",
accessorKey: "status",
meta: {
type: ColumnFilterType.SINGLE_SELECT,
icon: "circle-dot", // Optional icon
} as ColumnFilterMeta,
},
{
id: "createdAt",
header: "Created",
accessorKey: "createdAt",
meta: {
type: ColumnFilterType.DATE_RANGE,
dateRangeOptions: {
range: "past",
convertToTimeZone: true,
},
} as ColumnFilterMeta,
},
];
```
### Faceted Filters with `getFacetedUniqueValues`
For select filters that need dynamic options based on data, use `getFacetedUniqueValues` in your table configuration:
```tsx
// In your table configuration
const table = useReactTable({
// ... other options
getFacetedUniqueValues: (_, columnId) => () => {
switch (columnId) {
case "teamId":
return convertFacetedValuesToMap(
teams.map(team => ({
label: team.name,
value: team.id
}))
);
case "role":
return convertFacetedValuesToMap([
{ label: "Admin", value: "admin" },
{ label: "Member", value: "member" }
]);
default:
return new Map();
}
}
});
```
**Custom Faceted Values Hook Example:**
From `apps/web/modules/bookings/hooks/useFacetedUniqueValues.ts`:
```tsx
export function useFacetedUniqueValues() {
const eventTypes = useEventTypes();
const { data: teams } = trpc.viewer.teams.list.useQuery();
const { data: members } = trpc.viewer.teams.listSimpleMembers.useQuery();
return useCallback(
(_: Table<any>, columnId: string) => (): Map<FacetedValue, number> => {
if (columnId === "eventTypeId") {
return convertFacetedValuesToMap(eventTypes || []);
} else if (columnId === "teamId") {
return convertFacetedValuesToMap(
(teams || []).map((team) => ({
label: team.name,
value: team.id,
}))
);
} else if (columnId === "userId") {
return convertFacetedValuesToMap(
(members || [])
.map((member) => ({
label: member.name,
value: member.id,
}))
.filter((option): option is { label: string; value: number } => Boolean(option.label))
);
}
return new Map<FacetedValue, number>();
},
[eventTypes, teams, members]
);
}
```
**Usage in Table Configuration:**
```tsx
// From packages/features/users/components/UserTable/UserListTable.tsx
const table = useReactTable({
// ... other options
getFacetedUniqueValues: (_, columnId) => () => {
if (facetedTeamValues) {
switch (columnId) {
case "role":
return convertFacetedValuesToMap(facetedTeamValues.roles);
case "teamId":
return convertFacetedValuesToMap(facetedTeamValues.teams);
default:
return new Map();
}
}
return new Map();
}
});
```
### Filter Components
#### FilterBar
Displays active filters and add filter button:
```tsx
<DataTableFilters.FilterBar table={table} />
```
#### Individual Filter Components
```tsx
// Add filter button
<DataTableFilters.AddFilterButton table={table} />
// Active filters display
<DataTableFilters.ActiveFilters table={table} />
// Clear all filters
<DataTableFilters.ClearFiltersButton />
// Column visibility toggle
<DataTableFilters.ColumnVisibilityButton table={table} />
```
## Segment System
Segments allow users to save and share filter configurations. There are two types:
**Important:** Filter segments are only enabled when you pass the `useSegments` prop to `DataTableProvider`:
```tsx
<DataTableProvider
useSegments={useSegments} // Required to enable segments
>
{/* Your table content */}
</DataTableProvider>
```
Without the `useSegments` prop, segment functionality will be disabled and segment-related components will not be available.
### System Segments
Predefined segments created by developers:
```tsx
const systemSegments: SystemFilterSegment[] = [
{
id: "active-users",
name: "Active Users",
type: "system",
activeFilters: [
{
f: "status",
v: {
type: ColumnFilterType.SINGLE_SELECT,
data: "active"
}
}
],
sorting: [{ id: "lastLogin", desc: true }],
}
];
<DataTableProvider
systemSegments={systemSegments}
>
{/* ... */}
</DataTableProvider>
```
### User Segments
Segments saved by users with personal or team scope:
```tsx
// Personal segment (scope: "USER")
// Team segment (scope: "TEAM")
```
### Segment Components
```tsx
// Segment selector dropdown
<DataTableSegment.Select />
// Save current state as segment
<DataTableSegment.SaveButton />
// Segment management (rename, duplicate, delete)
// Available in the segment dropdown submenu
```
### Segment Permissions
- **Personal segments**: Only visible to the creator
- **Team segments**: Visible to all team members
- **System segments**: Visible to all users
- **Admin actions**: Rename/delete require admin/owner permissions
## Pagination Modes
### Standard Pagination (Recommended)
Traditional page-based pagination is the recommended approach:
```tsx
<DataTableWrapper
paginationMode="standard"
totalRowCount={totalCount}
table={table}
/>
```
**Features:**
- Page numbers and navigation
- Configurable page sizes
- Total count display
- Reliable performance
- Better user experience
- No virtualization issues
**Why Standard Mode is Preferred:**
Standard pagination provides a more predictable and stable user experience. It avoids the complexity and potential issues associated with virtualized infinite scrolling.
### Infinite Pagination (Use with Caution)
Alternative infinite scroll mode with known limitations:
```tsx
<DataTableWrapper
paginationMode="infinite"
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetching={isFetching}
table={table}
/>
```
**Features:**
- Automatic loading on scroll
- Virtualized rendering
- Fixed container height (80dvh)
**Known Issues:**
- Virtualized infinite loading has several problems
- Can cause performance and UX issues
- Standard mode was introduced to address these problems
- Use only when absolutely necessary and with thorough testing
## Toolbar Components
### DataTableToolbar
Container and utility components for table toolbars:
```tsx
// Toolbar container
<DataTableToolbar.Root>
<DataTableToolbar.SearchBar />
<DataTableToolbar.CTA color="primary" StartIcon="plus">
Add User
</DataTableToolbar.CTA>
</DataTableToolbar.Root>
// Search input with debounced updates
<DataTableToolbar.SearchBar className="max-w-48" />
// Clear filters button (auto-hides when no filters)
<DataTableToolbar.ClearFiltersButton />
// Custom action button
<DataTableToolbar.CTA
color="secondary"
StartIcon="download"
onClick={handleExport}
>
Export
</DataTableToolbar.CTA>
```
### DataTableSelectionBar
For bulk actions when rows are selected:
```tsx
{numberOfSelectedRows > 0 && (
<DataTableSelectionBar.Root>
<p>{t("number_selected", { count: numberOfSelectedRows })}</p>
<DataTableSelectionBar.Button
color="destructive"
icon="trash-2"
onClick={handleBulkDelete}
>
Delete Selected
</DataTableSelectionBar.Button>
</DataTableSelectionBar.Root>
)}
```
## Advanced Usage
### Server-Side Operations
The DataTable system is designed for **server-side filtering, sorting, and pagination** with standard pagination. This approach is necessary because we only fetch a limited number of items per page, unlike the previous infinite scrolling approach that cached large amounts of data and could filter on the client side.
#### Basic Server-Side Pattern
```tsx
// Get current table state for API calls
const { limit, offset, sorting } = useDataTable();
const columnFilters = useColumnFilters();
// Use in your API call
const { data } = trpc.users.list.useQuery({
limit,
offset,
sorting,
filters: columnFilters
});
```
#### Prisma Where Clause Construction
For Prisma-based backends, extract individual filters and build typed where conditions:
```tsx
// From packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
const roleFilter = filters.find((filter) => filter.id === "role") as
| TypedColumnFilter<ColumnFilterType.MULTI_SELECT>
| undefined;
const teamFilter = filters.find((filter) => filter.id === "teams") as
| TypedColumnFilter<ColumnFilterType.MULTI_SELECT>
| undefined;
const lastActiveAtFilter = filters.find((filter) => filter.id === "lastActiveAt") as
| TypedColumnFilter<ColumnFilterType.DATE_RANGE>
| undefined;
const whereClause: Prisma.MembershipWhereInput = {
user: {
...(teamFilter && {
teams: {
some: {
team: makeWhereClause({
columnName: "name",
filterValue: teamFilter.value,
}),
},
},
}),
...(lastActiveAtFilter &&
makeWhereClause({
columnName: "lastActiveAt",
filterValue: lastActiveAtFilter.value,
})),
},
teamId: organizationId,
...(roleFilter && makeWhereClause({
columnName: "role",
filterValue: roleFilter.value,
})),
};
```
#### Raw SQL Optimization
For performance-critical queries, use raw SQL with `makeSqlCondition`:
```tsx
// From packages/lib/server/service/InsightsRoutingBaseService.ts
async getFilterConditions(): Promise<Prisma.Sql | null> {
const conditions: Prisma.Sql[] = [];
const columnFilters = this.filters.columnFilters || [];
// Convert columnFilters array to object for easier access
const filtersMap = columnFilters.reduce((acc, filter) => {
acc[filter.id] = filter;
return acc;
}, {} as Record<string, TypedColumnFilter<ColumnFilterType>>);
// Extract booking status order filter
const bookingStatusOrder = filtersMap["bookingStatusOrder"];
if (bookingStatusOrder && isMultiSelectFilterValue(bookingStatusOrder.value)) {
const statusCondition = makeSqlCondition(bookingStatusOrder.value);
if (statusCondition) {
conditions.push(Prisma.sql`rfrd."bookingStatusOrder" ${statusCondition}`);
}
}
// Extract booking UID filter
const bookingUid = filtersMap["bookingUid"];
if (bookingUid && isTextFilterValue(bookingUid.value)) {
const uidCondition = makeSqlCondition(bookingUid.value);
if (uidCondition) {
conditions.push(Prisma.sql`rfrd."bookingUid" ${uidCondition}`);
}
}
// Join all conditions with AND
return conditions.reduce((acc, condition, index) => {
if (index === 0) return condition;
return Prisma.sql`(${acc}) AND (${condition})`;
});
}
```
#### Advanced Parameter Manipulation
For complex cases where you need to manipulate filter data before sending to the backend, extract the logic into a separate hook:
```tsx
// packages/features/insights/hooks/useInsightsRoutingParameters.ts
export function useInsightsRoutingParameters() {
const { scope, selectedTeamId } = useInsightsOrgTeams();
// Get date range filter and manipulate it
const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data;
const startDate = useChangeTimeZoneWithPreservedLocalTime(
useMemo(() => {
return dayjs(createdAtRange?.startDate ?? getDefaultStartDate().toISOString())
.startOf("day")
.toISOString();
}, [createdAtRange?.startDate])
);
// Get other column filters excluding the manipulated ones
const columnFilters = useColumnFilters({
exclude: ["createdAt"],
});
return {
scope,
selectedTeamId,
startDate,
endDate,
columnFilters,
};
}
```
#### Key Hooks for Server-Side Integration
- **`useColumnFilters()`** - Get applied filters for backend requests
- **`useDataTable()`** - Get `limit`, `offset`, `sorting` for pagination
- **`useFilterValue(columnId, schema)`** - Get specific filter value with validation
- **Custom parameter hooks** - Extract complex manipulation logic
#### Server Utility Functions
The DataTable system provides utility functions for both Prisma and raw SQL approaches:
- **`makeWhereClause()`** - Converts filter values to Prisma where clause objects
- **`makeSqlCondition()`** - Converts filter values to raw SQL conditions
- **`makeOrderBy()`** - Converts sorting state to Prisma orderBy format
### Custom Hooks
#### useDataTable
Access the DataTable context:
```tsx
const {
activeFilters,
sorting,
columnVisibility,
pageIndex,
pageSize,
searchTerm,
selectedSegment,
} = useDataTable();
```
#### useColumnFilters
Get processed column filters for API calls:
```tsx
const columnFilters = useColumnFilters();
// Returns: ColumnFilter[] ready for backend consumption
// With exclusions (useful when manipulating specific filters)
const columnFilters = useColumnFilters({
exclude: ["createdAt", "dateRange"]
});
```
#### useFilterableColumns
Extract filterable columns from table definition:
```tsx
const filterableColumns = useFilterableColumns(table);
```
### Custom Filter Components
Create custom filter implementations:
```tsx
function CustomStatusFilter({ column }: { column: Column<any> }) {
const { updateFilter } = useDataTable();
return (
<Select
onValueChange={(value) =>
updateFilter(column.id, {
type: ColumnFilterType.SINGLE_SELECT,
data: value
})
}
>
{/* Custom filter UI */}
</Select>
);
}
```
### Portal Integration
Use portals for toolbar actions:
```tsx
const { ctaContainerRef } = useDataTable();
{ctaContainerRef.current && createPortal(
<div className="flex gap-2">
<Button>Custom Action</Button>
</div>,
ctaContainerRef.current
)}
```
## Real-world Examples
### Example 1: User Management Table
From `packages/features/users/components/UserTable/UserListTable.tsx`:
```tsx
<DataTableWrapper<UserTableUser>
testId="user-list-data-table"
table={table}
isPending={isPending}
totalRowCount={data?.meta?.totalRowCount}
paginationMode="standard"
ToolbarLeft={
<>
<DataTableToolbar.SearchBar />
<DataTableFilters.ColumnVisibilityButton table={table} />
<DataTableFilters.FilterBar table={table} />
</>
}
ToolbarRight={
<>
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</>
}
>
{/* Selection bar for bulk actions */}
{numberOfSelectedRows > 0 && (
<DataTableSelectionBar.Root>
<p>{t("number_selected", { count: numberOfSelectedRows })}</p>
<DeleteBulkUsers
users={table.getSelectedRowModel().flatRows.map(row => row.original)}
onRemove={() => table.toggleAllPageRowsSelected(false)}
/>
</DataTableSelectionBar.Root>
)}
</DataTableWrapper>
```
### Example 2: Bookings List
From `apps/web/modules/bookings/components/BookingsList.tsx`:
```tsx
<DataTableWrapper
className="mb-6"
tableContainerRef={tableContainerRef}
table={table}
testId={`${status}-bookings`}
bodyTestId="bookings"
headerClassName="hidden"
isPending={query.isPending}
totalRowCount={query.data?.totalCount}
variant="compact"
paginationMode="standard"
ToolbarLeft={
<DataTableFilters.FilterBar table={table} />
}
ToolbarRight={
<>
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</>
}
LoaderView={<SkeletonLoader />}
EmptyView={
<EmptyScreen
Icon="calendar"
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description")}
/>
}
/>
```
### Example 3: Team Member List with Infinite Scroll
From `packages/features/ee/teams/components/MemberList.tsx`:
```tsx
<DataTableWrapper
testId="team-member-list-container"
table={table}
tableContainerRef={tableContainerRef}
isPending={isPending}
paginationMode="infinite"
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetching={isFetching}
ToolbarLeft={
<>
<DataTableToolbar.SearchBar />
<DataTableFilters.FilterBar table={table} />
</>
}
ToolbarRight={
<>
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</>
}
>
{/* Bulk selection and actions */}
{numberOfSelectedRows > 0 && (
<DataTableSelectionBar.Root>
<TeamListBulkAction table={table} />
<MassAssignAttributesBulkAction table={table} filters={columnFilters} />
</DataTableSelectionBar.Root>
)}
</DataTableWrapper>
```
## TypeScript Types Reference
### Core Types
```tsx
// Filter value types
type FilterValue =
| SingleSelectFilterValue
| MultiSelectFilterValue
| TextFilterValue
| NumberFilterValue
| DateRangeFilterValue;
// Active filter structure
type ActiveFilter = {
f: string; // field/column ID
v?: FilterValue; // filter value
};
// Segment types
type SegmentIdentifier =
| { id: string; type: "system" }
| { id: number; type: "user" };
// Column filter metadata
type ColumnFilterMeta = {
type: ColumnFilterType;
icon?: IconName;
dateRangeOptions?: DateRangeFilterOptions;
textOptions?: TextFilterOptions;
};
```
### Filter Value Schemas
```tsx
// Single select
type SingleSelectFilterValue = {
type: ColumnFilterType.SINGLE_SELECT;
data: string | number;
};
// Multi select
type MultiSelectFilterValue = {
type: ColumnFilterType.MULTI_SELECT;
data: Array<string | number>;
};
// Text filter
type TextFilterValue = {
type: ColumnFilterType.TEXT;
data: {
operator: TextFilterOperator;
operand: string;
};
};
// Number filter
type NumberFilterValue = {
type: ColumnFilterType.NUMBER;
data: {
operator: NumberFilterOperator;
operand: number;
};
};
// Date range filter
type DateRangeFilterValue = {
type: ColumnFilterType.DATE_RANGE;
data: {
startDate: string | null;
endDate: string | null;
preset: string;
};
};
```
### Segment Types
```tsx
// System segment (predefined)
type SystemFilterSegment = {
id: string;
name: string;
type: "system";
activeFilters: ActiveFilters;
sorting?: SortingState;
columnVisibility?: Record<string, boolean>;
columnSizing?: Record<string, number>;
perPage?: number;
searchTerm?: string | null;
};
// User segment (saved by users)
type UserFilterSegment = FilterSegmentOutput & {
type: "user";
};
// Combined segment type
type CombinedFilterSegment = SystemFilterSegmentInternal | UserFilterSegment;
```
## Best Practices
### Performance
1. **Use standard pagination** for most use cases (recommended)
2. **Implement proper memoization** for column definitions
3. **Debounce search inputs** (handled automatically by DataTableToolbar.SearchBar)
4. **Consider infinite mode only for specific cases** where traditional pagination isn't suitable, but be aware of potential issues
### State Management
1. **Provide unique tableIdentifier** for each table instance
2. **Use segments** for complex filter combinations
3. **Persist user preferences** via the segment system
4. **Clear filters appropriately** when changing contexts
### User Experience
1. **Provide loading states** with LoaderView and EmptyView
2. **Use standard pagination** for consistent user experience
3. **Include search functionality** for text-heavy data
4. **Implement bulk actions** for management interfaces
5. **Show filter counts** and active filter indicators
### Accessibility
1. **Use semantic HTML** (handled by components)
2. **Provide proper ARIA labels** for custom filters
3. **Support keyboard navigation** (built-in)
4. **Test with screen readers**
### Code Organization
```tsx
// ✅ Good: Memoize column definitions
const columns = useMemo(() => [
{
id: "name",
header: "Name",
accessorKey: "name",
meta: { type: ColumnFilterType.TEXT },
},
], []);
// ✅ Good: Extract filter logic
const useUserFilters = () => {
const columnFilters = useColumnFilters();
return useMemo(() =>
transformFiltersForAPI(columnFilters),
[columnFilters]
);
};
// ✅ Good: Separate concerns
function UserTableContainer() {
const filters = useUserFilters();
const { data, isPending } = useUsers(filters);
return (
<DataTableProvider>
<UserTable data={data} isPending={isPending} />
</DataTableProvider>
);
}
```
### Common Patterns
#### Filter Integration with API
```tsx
function useTableData() {
const columnFilters = useColumnFilters();
const { sorting, pageIndex, pageSize, searchTerm } = useDataTable();
const queryParams = useMemo(() => ({
filters: columnFilters,
sorting,
page: pageIndex,
limit: pageSize,
search: searchTerm,
}), [columnFilters, sorting, pageIndex, pageSize, searchTerm]);
return useQuery({
queryKey: ['table-data', queryParams],
queryFn: () => fetchData(queryParams),
});
}
```
#### Custom Filter Options
```tsx
function useStatusFilterOptions() {
const { data: statuses } = useStatuses();
return useMemo(() =>
statuses?.map(status => ({
label: status.name,
value: status.id,
section: status.category,
})) || [],
[statuses]
);
}
```
#### Segment Presets
```tsx
const BOOKING_SEGMENTS: SystemFilterSegment[] = [
{
id: "upcoming",
name: "Upcoming",
type: "system",
activeFilters: [
{
f: "status",
v: { type: ColumnFilterType.SINGLE_SELECT, data: "confirmed" }
},
{
f: "startTime",
v: {
type: ColumnFilterType.DATE_RANGE,
data: { preset: "future", startDate: null, endDate: null }
}
}
],
sorting: [{ id: "startTime", desc: false }],
},
];
```
This guide covers the complete DataTable system. For specific implementation details, refer to the source files in `packages/features/data-table/` and the usage examples throughout the Cal.com codebase.