Files
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

38 KiB

DataTable System Guide

A comprehensive guide to using Cal.diy's DataTable system for building powerful, filterable, and paginated data tables.

Table of Contents

  1. Overview
  2. Quick Start
  3. Core Components
  4. Filter System
  5. Segment System
  6. Pagination Modes
  7. Toolbar Components
  8. Advanced Usage
  9. Real-world Examples
  10. TypeScript Types Reference
  11. 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

// Types and utilities stay in @calcom/features/data-table
import { ColumnFilterType } from "@calcom/features/data-table";

// Hooks, contexts, and providers are in apps/web/modules/data-table
// (use ~/data-table/... imports within apps/web)
import { DataTableProvider } from "~/data-table/DataTableProvider";
import { useDataTable } from "~/data-table/hooks/useDataTable";

// UI components are in apps/web/modules/data-table/components
import { DataTableWrapper } from "~/data-table/components/DataTableWrapper";
import { DataTableFilters } from "~/data-table/components/filters";

// 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

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:

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

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)

Separator Rows

The DataTable supports separator rows that act as visual group headers between regular data rows. Separators are useful for grouping related data and providing visual organization.

Type Definitions

export type SeparatorRow = {
  type: "separator";
  label: string;
  className?: string;
};

export type DataTableRow<TData> = TData | SeparatorRow;

export function isSeparatorRow<TData>(row: DataTableRow<TData>): row is SeparatorRow {
  return typeof row === "object" && row !== null && "type" in row && row.type === "separator";
}

Usage

Separator rows are defined in your data array alongside regular rows:

const data = [
  { type: "separator", label: "Active Users" },
  { id: 1, name: "Alice", status: "active" },
  { id: 2, name: "Bob", status: "active" },
  { type: "separator", label: "Inactive Users" },
  { id: 3, name: "Charlie", status: "inactive" },
];

<DataTableWrapper
  table={table}
  hideSeparatorsOnSort={true}
  separatorClassName="custom-separator"
  // ... other props
/>;

Configuration Options

Props

  • hideSeparatorsOnSort?: boolean (default: true) - Hide separators when sorting is active
  • hideSeparatorsOnFilter?: boolean (default: false) - Hide separators when filtering is active
  • separatorClassName?: string - Global CSS class applied to all separator rows

Per-Separator Styling

Each separator can have its own styling:

const data = [
  {
    type: "separator",
    label: "Active Users",
    className: "bg-green-50 text-green-900",
  },
  { id: 1, name: "Alice" },
  {
    type: "separator",
    label: "Inactive Users",
    className: "bg-red-50 text-red-900",
  },
  { id: 2, name: "Bob" },
];

Behavior with Sorting and Filtering

Default Behavior:

  • Separators are visible when no sorting/filtering is applied
  • Separators automatically hide when sorting is applied (if hideSeparatorsOnSort is true)
  • Separators reappear when sorting is cleared

Why Hide on Sort? When users sort by a column, the logical grouping may no longer make sense. For example, if you group by status but then sort by name, the status groups become meaningless. Hiding separators during sorting provides a cleaner, more intuitive experience.

Custom Behavior: You can disable automatic hiding by setting hideSeparatorsOnSort={false} and handle separator positioning in your data preparation logic.

Styling

Separator rows use the same styling as table headers and span the full width of the table:

.separator-row {
  display: flex;
  width: 100%;
  border: none;
}

.separator-row > div {
  background-color: var(--muted);
  font-weight: 600;
  color: var(--emphasis);
  padding: 0.5rem 0.75rem;
}

You can customize styling using:

  • separatorClassName prop for global styling
  • className property on individual separator objects
  • CSS targeting .separator-row class

Example: User Management with Status Groups

function UserTable() {
  const data = useMemo(() => {
    const users = getUsers();
    const groupedUsers = [];

    // Add active users group
    const activeUsers = users.filter((u) => u.status === "active");
    if (activeUsers.length > 0) {
      groupedUsers.push({ type: "separator", label: "Active Users" });
      groupedUsers.push(...activeUsers);
    }

    // Add inactive users group
    const inactiveUsers = users.filter((u) => u.status === "inactive");
    if (inactiveUsers.length > 0) {
      groupedUsers.push({ type: "separator", label: "Inactive Users" });
      groupedUsers.push(...inactiveUsers);
    }

    return groupedUsers;
  }, [users]);

  return (
    <DataTableWrapper
      table={table}
      hideSeparatorsOnSort={true}
      separatorClassName="uppercase text-xs tracking-wide"
    />
  );
}

Filter System

The DataTable supports 5 filter types with various operators and options.

Filter Types

1. Single Select Filter

{
  type: ColumnFilterType.SINGLE_SELECT,
  options: [
    { label: "Admin", value: "admin" },
    { label: "User", value: "user" },
    { label: "Guest", value: "guest" },
  ]
}

2. Multi Select Filter

{
  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

{
  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

{
  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

{
  type: ColumnFilterType.DATE_RANGE,
  dateRangeOptions: {
    range: "past", // or "custom"
    convertToTimeZone: true
  }
}

Column Filter Configuration

Add filtering to columns using the meta property:

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:

// 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 (hooks live in apps/web/modules/):

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:

// From apps/web/modules/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:

<DataTableFilters.FilterBar table={table} />

Individual Filter Components

// 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:

<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:

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:

// Personal segment (scope: "USER")
// Team segment (scope: "TEAM")

Segment Components

// 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

Traditional page-based pagination is the recommended approach:

<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:

<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:

// 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:

{
  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

// Hooks are imported from ~/data-table/hooks/ within apps/web
import { useDataTable } from "~/data-table/hooks/useDataTable";
import { useColumnFilters } from "~/data-table/hooks/useColumnFilters";

// 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:

// 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:

// 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:

// apps/web/modules/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 (import from ~/data-table/hooks/useColumnFilters)
  • useDataTable() - Get limit, offset, sorting for pagination (import from ~/data-table/hooks/useDataTable)
  • useFilterValue(columnId, schema) - Get specific filter value with validation (import from ~/data-table/hooks/useFilterValue)
  • Custom parameter hooks - Extract complex manipulation logic (place in apps/web/modules/)

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:

const { activeFilters, sorting, columnVisibility, pageIndex, pageSize, searchTerm, selectedSegment } =
  useDataTable();

useColumnFilters

Get processed column filters for API calls:

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:

const filterableColumns = useFilterableColumns(table);

Custom Filter Components

Create custom filter implementations:

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:

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 apps/web/modules/users/components/UserTable/UserListTable.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 (hooks/providers imported from ~/data-table/):

<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 apps/web/modules/ee/teams/components/MemberList.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

// 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

// 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

// 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

// ✅ 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

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

function useStatusFilterOptions() {
  const { data: statuses } = useStatuses();

  return useMemo(
    () =>
      statuses?.map((status) => ({
        label: status.name,
        value: status.id,
        section: status.category,
      })) || [],
    [statuses]
  );
}

Segment Presets

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:

  • Types, utilities, serializers, server-side code: packages/features/data-table/lib/
  • Hooks, contexts, DataTableProvider: apps/web/modules/data-table/
  • UI components (DataTable, DataTableWrapper, filters, etc.): apps/web/modules/data-table/components/
  • Usage examples: Throughout apps/web/modules/ (bookings, insights, users, etc.)