# 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 ( > } ToolbarRight={ <> > } /> ); } ``` ## 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; // Column management columnVisibility: VisibilityState; setColumnVisibility: OnChangeFn; columnSizing: ColumnSizingState; setColumnSizing: OnChangeFn; // 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 = { table: ReactTableType; 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; onRowMouseclick?: (row: Row) => 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, columnId: string) => (): Map => { 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(); }, [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 ``` #### Individual Filter Components ```tsx // Add filter button // Active filters display // Clear all filters // Column visibility toggle ``` ## 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 {/* Your table content */} ``` 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 }], } ]; {/* ... */} ``` ### 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 // Save current state as segment // 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 ``` **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 ``` **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 Add User // Search input with debounced updates // Clear filters button (auto-hides when no filters) // Custom action button Export ``` ### DataTableSelectionBar For bulk actions when rows are selected: ```tsx {numberOfSelectedRows > 0 && ( {t("number_selected", { count: numberOfSelectedRows })} Delete Selected )} ``` ## 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 | undefined; const teamFilter = filters.find((filter) => filter.id === "teams") as | TypedColumnFilter | undefined; const lastActiveAtFilter = filters.find((filter) => filter.id === "lastActiveAt") as | TypedColumnFilter | 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 { 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>); // 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 }) { const { updateFilter } = useDataTable(); return ( updateFilter(column.id, { type: ColumnFilterType.SINGLE_SELECT, data: value }) } > {/* Custom filter UI */} ); } ``` ### Portal Integration Use portals for toolbar actions: ```tsx const { ctaContainerRef } = useDataTable(); {ctaContainerRef.current && createPortal( Custom Action , ctaContainerRef.current )} ``` ## Real-world Examples ### Example 1: User Management Table From `packages/features/users/components/UserTable/UserListTable.tsx`: ```tsx testId="user-list-data-table" table={table} isPending={isPending} totalRowCount={data?.meta?.totalRowCount} paginationMode="standard" ToolbarLeft={ <> > } ToolbarRight={ <> > } > {/* Selection bar for bulk actions */} {numberOfSelectedRows > 0 && ( {t("number_selected", { count: numberOfSelectedRows })} row.original)} onRemove={() => table.toggleAllPageRowsSelected(false)} /> )} ``` ### Example 2: Bookings List From `apps/web/modules/bookings/components/BookingsList.tsx`: ```tsx } ToolbarRight={ <> > } LoaderView={} EmptyView={ } /> ``` ### Example 3: Team Member List with Infinite Scroll From `packages/features/ee/teams/components/MemberList.tsx`: ```tsx > } ToolbarRight={ <> > } > {/* Bulk selection and actions */} {numberOfSelectedRows > 0 && ( )} ``` ## 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; }; // 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; columnSizing?: Record; 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 ( ); } ``` ### 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.
{t("number_selected", { count: numberOfSelectedRows })}