Files
calendar/packages/features/data-table/GUIDE.md
T
Eunjae LeeGitHubeunjae@cal.com <hey@eunjae.dev>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
fddee71829 refactor: extract bookings list and calendar views (#24486)
* refactor: extract bookings list and calendar views with nuqs state management

- Extract list-related code into BookingsListView component
- Create empty BookingsCalendarView component for future implementation
- Add nuqs query param state management for view toggle (defaults to list)
- Update bookings-listing-view to conditionally render views
- No visible changes to users (list view remains default)

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

* refactor: move data fetching logic to parent component

- Keep useFilterValue calls, trpc query, columns, flatData, bookingsToday, finalData, and table setup in parent component
- BookingsListView now receives data as props instead of fetching it
- This allows both list and calendar views to share the same data source

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

* fix: add customView column back to render booking items

The customView column was inadvertently removed during refactoring. This column is crucial as it renders the actual BookingListItem components, the "today" header, and the "next" header for the bookings list.

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

* refactor: rename files for better clarity

- Renamed bookings-listing-view.tsx → bookings-view.tsx (parent view)
- Renamed bookings-list-view.tsx → BookingsList.tsx (list component)
- Renamed bookings-calendar-view.tsx → BookingsCalendar.tsx (calendar component)
- Moved list and calendar components from views/ to components/ directory
- Updated all imports to reflect new structure

This creates a clearer hierarchy where -view is the orchestrator and components are the renderers.

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

* clean up implementation

* clean up types

* revert unnecessary changes

* Update packages/features/data-table/GUIDE.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-21 11:09:47 +00:00

1288 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
});
return (
<DataTableProvider tableIdentifier="user-table">
<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
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
tableIdentifier="users"
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}
tableIdentifier="user-table"
>
{/* ... */}
</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 tableIdentifier="users">
<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.