Skip to content

DataTable

DataTable is a compound component for displaying collections of records. It integrates with the collection variable hooks (useCollectionVariables) to drive sorting, filtering, and cursor-based pagination through a GraphQL API.

Import

tsx
import {
  DataTable,
  useDataTable,
  useDataTableContext,
  useCollectionVariables,
  createColumnHelper,
  type Column,
  type DataTableData,
  type DataTableRootProps,
  type DataTablePaginationProps,
  type RowAction,
  type UseDataTableOptions,
  type UseDataTableReturn,
  type MetadataFieldOptions,
  type DataTableContextValue,
} from "@tailor-platform/app-shell";

Basic Usage

tsx
import { gql, useQuery } from "urql";
import {
  DataTable,
  useDataTable,
  useCollectionVariables,
  createColumnHelper,
} from "@tailor-platform/app-shell";

const LIST_JOURNALS = gql`
  query ListJournals(
    $after: String
    $before: String
    $first: Int
    $last: Int
    $order: [JournalOrderInput]
    $query: JournalQueryInput
  ) {
    journals(
      after: $after
      before: $before
      first: $first
      last: $last
      order: $order
      query: $query
    ) {
      edges {
        node {
          id
          contents
          authorID
        }
      }
      pageInfo {
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
      total
    }
  }
`;

type Journal = { id: string; contents: string; authorID: string };

const { column } = createColumnHelper<Journal>();

const columns = [
  column({
    label: "ID",
    render: (row) => row.id,
    filter: { field: "id", type: "uuid" },
  }),
  column({
    label: "Author",
    render: (row) => row.authorID,
    sort: { field: "authorID", type: "string" },
    filter: { field: "authorID", type: "string" },
  }),
  column({
    label: "Contents",
    render: (row) => row.contents,
    filter: { field: "contents", type: "string" },
  }),
];

function JournalsPage() {
  const { variables, control } = useCollectionVariables({
    params: { pageSize: 20 },
  });

  const [result] = useQuery({
    query: LIST_JOURNALS,
    variables: {
      ...variables.pagination,
      query: variables.query,
      order: variables.order,
    },
  });

  const table = useDataTable({
    columns,
    data: result.data
      ? {
          rows: result.data.journals.edges.map((e) => e.node),
          pageInfo: result.data.journals.pageInfo,
          total: result.data.journals.total,
        }
      : undefined,
    loading: result.fetching,
    control,
  });

  return (
    <DataTable.Root value={table}>
      <DataTable.Toolbar>
        <DataTable.Filters />
      </DataTable.Toolbar>
      <DataTable.Table />
      <DataTable.Footer>
        <DataTable.Pagination pageSizeOptions={[10, 20, 50]} />
      </DataTable.Footer>
    </DataTable.Root>
  );
}

Sub-components

DataTable is a namespace object. All sub-components read state from DataTable.Root via context.

Sub-componentDescription
DataTable.RootContext provider. Wraps all other sub-components. Required.
DataTable.TableRenders the <table> with headers and body. Required.
DataTable.ToolbarContainer for toolbar content (e.g. filters, column visibility). Optional.
DataTable.FiltersAuto-generated filter chips from column filter configs. Requires control from useCollectionVariables.
DataTable.FooterFooter container for pagination and other footer content. Optional.
DataTable.PaginationPre-built pagination controls with optional row count and selection info. Requires control from useCollectionVariables. Place inside DataTable.Footer.

DataTable.Root Props

PropTypeDescription
valueUseDataTableReturn<TRow>Return value of useDataTable(). Required.
childrenReactNodeSub-components to render inside the root.
classNamestringAdditional CSS class for the root container.

DataTable.Pagination Props

PropTypeDefaultDescription
pageSizeOptionsnumber[]Available page-size options. When provided, a page-size switcher is rendered. First/Last buttons are shown only when the backend returns a total count.

DataTable.Pagination automatically displays a row count and selection info text on the left side of the pagination bar based on context state:

ConditionDisplayed text
total is providedX row(s)
Rows selected and total is providedY of X row(s) selected
Rows selected and total is not providedY row(s) selected
No selection enabled and no total(nothing displayed)

Row selection is enabled by providing onSelectionChange to useDataTable. The total value comes from DataTableData.total.

useDataTable

Creates the table state object to pass to DataTable.Root.

tsx
const table = useDataTable({
  columns,
  data,
  loading,
  control,
});

Options

OptionTypeDescription
columnsColumn<TRow>[]Column definitions. Required.
dataDataTableData<TRow> | undefinedFetched data. Pass undefined while loading.
loadingbooleanWhen true, renders a loading skeleton.
errorError | nullWhen set, renders an error message in the table body.
controlCollectionControlCollection control from useCollectionVariables(). Required for DataTable.Pagination and DataTable.Filters.
onClickRow(row: TRow) => voidCalled when the user clicks a row. Adds a pointer cursor to rows.
rowActionsRowAction<TRow>[]Per-row action items rendered in a kebab-menu column. The column is omitted when empty or not provided.
onSelectionChange(ids: string[]) => voidCalled with selected row IDs on change. Providing this enables the checkbox column. Rows must have a string id.
sortfalse | { multiple?: boolean }Sort behaviour. false disables sorting entirely. { multiple: true } enables multi-column sorting. Omit or pass {} for single-column sort (default).

DataTableData

PropertyTypeDescription
rowsTRow[]Row data to display.
pageInfoPageInfoCursor pagination info from the API.
totalnumber | nullTotal record count. Used for First/Last navigation and page counter.

Column

A column definition passed to useDataTable.

PropertyTypeDescription
labelstringColumn header text. Omit for icon-only columns.
render(row: TRow) => ReactNodeRenders the cell content. Required.
idstringStable identifier for column visibility and React key. Falls back to label when omitted.
widthnumberFixed column width in pixels. Optional.
accessor(row: TRow) => unknownExtracts the raw value for sorting. Not used for rendering.
sortSortConfigSort configuration. When set, the column header becomes clickable (Asc → Desc → off).
filterFilterConfigFilter configuration. When set, the column appears as an option in DataTable.Filters.

RowAction

PropertyTypeDescription
idstringStable identifier for the action.
labelstringDisplay label in the kebab menu.
iconReactNodeOptional icon shown beside the label.
variant"default" | "destructive"Visual style of the menu item.
isDisabled(row: TRow) => booleanReturn true to disable the action for a given row.
onClick(row: TRow) => voidCalled when the action is clicked.

createColumnHelper

Factory that captures the row type once and returns column and inferColumns with TRow already bound. Prefer this over the standalone column() function to avoid repeating the generic parameter.

tsx
const { column, inferColumns } = createColumnHelper<Order>();

column(options)

Defines a column with an explicit render function.

tsx
column({ label: "Name", render: (row) => row.name });
column({ label: "Actions", render: (row) => <button>Edit {row.name}</button> });

inferColumns(tableMetadata)

Binds table metadata and returns a per-field column factory. The factory derives label, sort, and filter config automatically from the field's metadata. Requires metadata generated by @tailor-platform/app-shell-sdk-plugin.

tsx
const infer = inferColumns(tableMetadata.order);

const columns = [
  column(infer("title")),
  column(infer("status")),
  column({ ...infer("createdAt"), render: (row) => formatDate(row.createdAt) }),
];

The factory accepts an optional second argument to override per-column defaults:

OptionTypeDefaultDescription
labelstringField description or name from metadataOverride the column header text.
widthnumberFixed column width in pixels.
sortbooleantrueSet to false to suppress the auto-generated sort config.
filterbooleantrueSet to false to suppress the auto-generated filter config.

useCollectionVariables

Manages collection query state (filters, sort, pagination) and derives variables for GraphQL queries.

tsx
const { variables, control } = useCollectionVariables({
  params: { pageSize: 20 },
});

// variables.pagination → { first, after? } or { last, before? }
// variables.query      → filter input object or undefined
// variables.order      → sort input array or undefined

Options

OptionTypeDescription
params.pageSizenumberInitial page size. Default: 20.
params.initialFiltersFilter[]Filters applied on first render.
params.initialSortSortState[]Sort applied on first render.
tableMetadataTableMetadataGenerated table metadata. Required for typed GraphQL documents (see Typed query variables).

Return Value

PropertyTypeDescription
variablesCollectionVariablesDerived query, order, and pagination sub-properties.
controlCollectionControlState and methods for filter, sort, and pagination management.

useCollectionVariables is decoupled from DataTable by design — the hook owns only query state and exposes plain variables. Any collection-based view (Kanban, Gantt, custom components) can use the same hook without modification.

Typed query variables

When using typed GraphQL documents (TypedDocumentNode), pass tableMetadata to useCollectionVariables. This narrows variables.query and variables.order from unknown to the precise types expected by the generated document.

tsx
import { tableMetadata } from "@/generated/app-shell-datatable.generated";

const { variables, control } = useCollectionVariables({
  tableMetadata: tableMetadata.order,
  params: { pageSize: 20 },
});

// variables.query is now BuildQueryVariables<typeof tableMetadata.order>
// variables.order is now { field: OrderableFieldName; direction: "Asc" | "Desc" }[]
const [result] = useQuery({
  query: LIST_ORDERS, // TypedDocumentNode — variables are fully type-checked
  variables: {
    ...variables.pagination,
    query: variables.query,
    order: variables.order,
  },
});

useDataTableContext

Accesses the full DataTable state from any component rendered inside DataTable.Root. Use this to build custom sub-components when the built-in ones don't fit.

tsx
import { useDataTableContext } from "@tailor-platform/app-shell";

function MyCustomPagination() {
  const { pageInfo, goToNextPage, goToPrevPage, hasNextPage, hasPrevPage } = useDataTableContext();
  // ...
}

SDK Plugin (@tailor-platform/app-shell-sdk-plugin)

The SDK plugin generates tableMetadata from TailorDB type definitions at code-gen time. This metadata bridges your schema to the DataTable — it specifies how each field should be rendered and filtered (e.g. date pickers for datetime fields, dropdown for enum fields).

Register the plugin in tailor.config.ts and run tailor-sdk generate:

ts
import { definePlugins } from "@tailor-platform/sdk";
import { appShellPlugin } from "@tailor-platform/app-shell-sdk-plugin";

export const plugins = definePlugins(
  appShellPlugin({
    dataTable: {
      metadataOutputPath: "src/generated/app-shell-datatable.generated.ts",
    },
  }),
);

The generated file exports tableMetadata, tableNames, and TableName. Pass tableMetadata to inferColumns to get type-safe column definitions with filter editors automatically configured:

ts
import { tableMetadata } from "@/generated/app-shell-datatable.generated";
import { createColumnHelper } from "@tailor-platform/app-shell";

const { column, inferColumns } = createColumnHelper<Order>();
const infer = inferColumns(tableMetadata.order);

const columns = [
  column(infer("title")), // string → text filter
  column(infer("status")), // enum   → dropdown filter with generated values
  column(infer("createdAt")), // datetime → date picker filter
];
  • CsvImporter — Guided CSV import flow
  • Table — Low-level table primitives used internally by DataTable