Hooks

useDocyrusDataGrid

One-call wiring of a Docyrus data source to a fully configured DataGrid + toolbar (DataGridViewSelect, search, filters, group, sort, row height, display) — including row fetching with view-derived query parameters.

Installation

pnpm dlx @docyrus/cli add @docyrus/hooks-use-docyrus-data-grid
Required Packages(5 packages)
pnpm add @docyrus/app-utils @docyrus/api-client @tanstack/react-query @tanstack/react-table react-querybuilder

This hook is distributed as source. It requires an authenticated RestApiClient from @docyrus/api-client and a QueryClientProvider from @tanstack/react-query somewhere above your component tree.

Overview

useDocyrusDataGrid is the one-call entry point that wires a Docyrus data source to <DataGrid> and produces a ready-to-render toolbar with the standard arrangement:

  • Left: DataGridViewSelect (saved views from the backend, wired through useDocyrusDataViewSelect), inline search input, filter menu.
  • Right: column grouping, sort, row height, display (grid/gallery), reload.

It also generates ColumnDefs automatically from the data source's field metadata, mapping every Docyrus field type (field-text, field-status, field-enum, field-multiSelect, field-money, field-rating, etc.) to the matching DataGrid cell variant.

Backend connection

The hook supports three ways to feed rows to the grid (precedence top→bottom):

  1. data — a pre-resolved array. Use when records come from somewhere outside of TanStack Query.
  2. collection — a TanStack DB collection generated by @docyrus/tanstack-db-generator (e.g., the result of useBaseOrganizationCollection()). The hook calls collection.list(resolvedListParams) inside its own useQuery.
  3. Default — GET /v1/apps/:appSlug/data-sources/:dataSourceSlug/items via the @docyrus/api-client you already passed as client. No extra setup.

The reload button refetches the items query (in modes 2 and 3), the data source + views queries, and any onReload callback you provided. In mode 1 the hook can't refetch rows on its own — wire onReload to your fetcher.

Tenant-aware formatting

Date / datetime / number cells render through the formatters supplied by <DocyrusTenantProvider> — no per-grid wiring required. Mount the provider once near the root of your app and every useDocyrusDataGrid call below it picks up the tenant's dateFormat, dateTimeFormat, decimalSeparator, thousandSeparator, decimalPrecision, and timeZone settings automatically.

Resolution order for each cell formatter (date / datetime / number):

  1. Explicit prop on useDocyrusDataGrid({ formatDate, formatDateTime, formatNumber }) — wins when supplied.
  2. Surrounding useDateFormat() / useNumberFormat() context (typically installed by <DocyrusTenantProvider>).
  3. The cell's own legacy fallback (Intl.NumberFormat for currency / percent in NumberCell, raw-string display for dates) — used only when neither a prop nor a real provider is present.

Apps that haven't adopted <DocyrusTenantProvider> yet keep their previous behaviour byte-for-byte (step 3). The migration is purely additive.

Relation cell icons / logos

field-relation cells render only the related record's name by default. To prefix each cell with the related record's display icon (or logo image), pass a relationIconFields map that points each relation field slug to the column slug on the related data source that holds the icon value:

useDocyrusDataGrid({
  client,
  appSlug: 'base',
  dataSourceSlug: 'contact',
  collection,
  relationIconFields: {
    organization: 'company_icon',  // field-icon on the org DSO
    country: 'flag_image'           // field-image on the country DSO
  }
});

The hook expands the items-query projection from organization(id, name, autonumber_id) to organization(id, name, autonumber_id, company_icon) so the server returns the icon value alongside the name in a single request. RelationCell then auto-detects the value shape:

  • field-icon (string identifier like 'huge ai-brain-03') → resolved via <DocyrusIcon>.
  • field-image (URL string, single { signed_url, file_name } object, or an array thereof) → rendered as a small <img> thumbnail.

The prefix is suppressed right after the user picks a new value (until the next refetch hydrates the row), since the freshly-picked option doesn't carry the icon field yet.

Borrowed relation columns

A grid lists one data source, but the field the user actually wants to see often lives one hop away — "the Matter's Project Lead next to each time entry". Set enableRelationColumns and every relation field on the schema is paired with every non-relation field of the data source it points at, offered as an extra column in the Fields menu:

useDocyrusDataGrid({
  client,
  appSlug: 'base',
  dataSourceSlug: 'time_entry',
  collection,
  enableRelationColumns: true
});

Off by default: it costs one extra request per data source (GET /v1/dev/data-sources/:id/fields?expand=parent) and widens the Fields menu considerably. The borrowed columns start hidden, so the grid looks and queries exactly as before until the user picks one.

What the user sees

The Fields menu groups borrowed columns under the relation they arrive through — Matter, Billing Contact, Case Handler 1 — with the grid's own fields in the first, unlabelled group.

Two relation fields pointing at the same data source stay separate groups. That is deliberate: on a schema where six different relation fields all target contact, "Billing Contact's email" and "Case Handler 1's email" are distinct columns and never collapse into one.

What goes on the wire

Picking a borrowed column does not issue a second request — its field joins that relation's existing projection group, and the backend resolves it in the same /items call:

columns=id,date,matter(id,name,project_lead)

row.matter.project_lead comes back already resolved to { id, name }. The three grammars are not interchangeable, and the hook maps between them for you:

PurposeToken
Column id + projectionmatter(project_lead)
Sort (orderBy.field)matter.project_lead
Filter (filters[].field)rel_matter/project_lead

One hop only

Target fields that are themselves relations are not offered. The backend's projection parser does not understand nested paths (rel_a(rel_b(x))), so a two-hop column would be pickable and then fail at query time — it is dropped from the catalogue instead.

Filtering a borrowed column is positive-only

Borrowed columns are filterable, but with a restricted operator set and always ANDed — never inside an OR group. This is a chosen contract, not a stopgap, and it follows from how the backend compiles a rel_ condition:

  • A rel_ condition is a query-level join, drained once into the FROM clause. It narrows the row set before the boolean structure of the WHERE tree is evaluated — so an OR branch containing a relation condition annihilates the whole OR: the other branch can match perfectly and the row still vanishes.
  • Negation lies under three-valued logic. "Matter type is not Litigation" compiles to a predicate that is false for a time entry with no matter at all, so those rows silently disappear and the user is shown a smaller number than the sentence they built promises. not contains is worse — its is null or … arm can make an exclusion filter grow the result set.

So the surface offers positive operators only (equals, contains, in, comparisons, safe relative dates). A relation rule the user drags into an OR group, or an operator outside the whitelist, is dropped before the request rather than sent and answered incorrectly.

Query parameters sent to the backend

Both backend modes (collection + direct fetch) call the items endpoint with a ZodSelectQueryPayload shaped object — see the full reference in docs/guides/docyrus-api-query-guide.md. The hook builds this payload (resolvedListParams) automatically from the active view + the toolbar search input + your listParams overrides:

Backend paramSourceNotes
columnsactiveView.columnVisibility + dataSource.fieldsComma-separated slugs of all visible fields, with id always first. Omitted if no fields are loaded yet.
orderByactiveView.sorting (TanStack ColumnSort[])Mapped to [{ field, direction: 'asc' | 'desc' }, …]. Omitted when empty.
filtersactiveView.filterQuery + side panel RuleGroupType + toolbar filter rulesAll sources are ANDed together. The shape matches IQueryFilterGroup. Omitted when no source produces any rules. See Side panel filters.
filterKeywordtoolbar search input (debounced)Trimmed; omitted when empty.
limit, offsetdefaultLimit option (default 100) + 0Override via listParams.
relation projectionvisible borrowed columns (enableRelationColumns)Merged into the matching relation's group inside columns, e.g. matter(id,name,project_lead). See Borrowed relation columns.
relation sort / filterborrowed column sort + filter rulesSort sends matter.project_lead; filters send rel_matter/project_lead, AND-only and positive-operator-only.
advanced filter grouptoolbar advanced filter panelANDed with the chip-bar rules under a reserved columnFilters entry. See Advanced AND/OR filtering.
anything elselistParamsSpread last and wins on conflict (use it for expand, fullCount, cursorDateStart/End, etc.).

The @docyrus/api-client jsonToQueryString helper auto-serializes object/array values via JSON.stringify, so complex filters and orderBy go on the wire as JSON strings without extra wiring.

The fully-resolved payload is exposed back to the consumer as resolvedListParams so you can inspect or persist it (e.g., for a "copy as cURL" button).

Usage

'use client';

import { useDocyrusAuth } from '@docyrus/signin';

import { DataGrid } from '@docyrus/ui/components/data-grid';
import { useDocyrusDataGrid } from '@docyrus/ui/library/hooks/use-docyrus-data-grid';

type OrganizationRow = { id: string; name: string };

export function OrganizationsGrid() {
  const { client } = useDocyrusAuth();

  if (!client) return null;

  // Direct API mode (no codegen needed):
  const { table, gridProps, toolbar } = useDocyrusDataGrid<OrganizationRow>({
    client,
    appSlug: 'crm',
    dataSourceSlug: 'organization'
  });

  // Collection mode (with @docyrus/tanstack-db-generator):
  // const collection = useCrmOrganizationCollection();
  // const { table, gridProps, toolbar } = useDocyrusDataGrid<OrganizationRow>({
  //   client,
  //   appSlug: 'crm',
  //   dataSourceSlug: 'organization',
  //   collection
  // });

  return (
    <div className="flex h-full flex-col gap-3">
      {toolbar}
      <DataGrid table={table} {...gridProps} height="auto" />
    </div>
  );
}

API Reference

Parameters

The hook accepts every option from useDocyrusDataViewSelect (for view CRUD + filter fields), plus:

DB-free metadata. Because this hook forwards into useDocyrusDataViewSelect, it also accepts dataSource (inject a pre-resolved schema → skips the getBySlug fetch), enableDataViews: false (skip the /views fetch), and dataSourceExpand (tune/drop the schema expand param). Pass dataSource together with data (pre-resolved rows) to render the grid with no metadata requests at all — ideal for system data sources without a schema route. See DB-free metadata.

OptionTypeDefaultDescription
dataArray<TData>—Pre-resolved rows. When provided, the hook skips its internal items query.
collection{ list: (params?) => Promise<…> }—TanStack DB collection (e.g., useBaseOrganizationCollection()); list(resolvedListParams) is called inside useQuery.
listParamsDocyrusDataGridListParams—Extra query params merged on top of the view-derived payload. Use it for expand, limit/offset, fullCount, etc. listParams.filters is AND-merged into the saved-view / toolbar / side-filter chain (does not replace).
pivotFiltersReadonlyArray<DocyrusPivotFilterGroupField>—Stack one or more pivot filter strips above the grid toolbar. Each entry configures a <PivotFilter> (status, priority, user, date bucket, …) via useDocyrusPivotFilter internally. The strips render inside the returned toolbar automatically, selections AND-merge into the items query filter, and pill counts cross-react bidirectionally. See Pivot filter integration.
defaultLimitnumber100Default page size when no limit is supplied via listParams.
enableItemsQuerybooleantrue when no dataToggle the internal items query.
showSelectColumnbooleantrueShow the row-select checkbox column as the first column.
enableRowMarkersbooleantrueShow 1-based row markers on the select column when not selected.
selectColumnColumnDef<TData>getDataGridSelectColumn({ size: 44, minSize: 44, enableRowMarkers })Override the default select column entirely.
actionsColumnColumnDef<TData>—Optional actions column rendered as the second column (right after select). Build it with getDataGridActionsColumn({ cell: … }).
extraColumnsArray<ColumnDef<TData>>—Extra columns prepended after the select + actions columns.
defaultRowGroupingColumnstring—Field slug used as the default row-grouping column when the active view has no grouping. Eligible field types: field-select, field-status, field-relation, field-date, field-dateTime, field-user.
mapColumn(field, defaultColumn) => ColumnDef | null—Per-field column override. Return null to skip a field.
dynamicLabelTranslator(label: string) => string—Translate / override every column header before render. Called once per column with the schema label; return the text to display. Applied after mapColumn. Only string headers are translated (custom header render functions are left untouched). See Translating column headers.
dynamicEnumOptionTranslator(option: EnumOption, field: IField) => string—Translate / override every enum option label (cells, filter dropdowns, row-group values) from one place. Called once per option; return the display text. Key by enums.<field.slug>.<option.slug>; slug / color / icon are preserved. See Translating enum options.
enableSearchbooleanfalseForwarded to useDataGrid (enables Cmd+F floating search inside the grid).
enableGroupingbooleantrueForwarded to useDataGrid. Required for the row-grouping picker to populate; only override if you don't want grouping at all.
readOnlybooleantrueForwarded to useDataGrid.
enableViewSelect / enableSearchInput / enableFilterMenu / enableGroupMenu / enableSortMenu / enableRowHeightMenu / enableFieldsMenu / enableDisplayMenu / enableReloadButtonbooleantrueToggle individual toolbar items. The toolbar filter menu (enableFilterMenu) and the side filter panel (enableSideFilters) are independent — both can be active at the same time. enableFieldsMenu mounts the field-visibility popover (column toggles for table view, body-field toggles for gallery view).
enableSideFiltersbooleanfalseActivate the side-panel filter rail rendered alongside the grid. When true, the hook returns a ready-to-render sideFilters element wired to <DataTableSideFilters> and merges its emitted RuleGroupType into the items request alongside the saved view filter and toolbar filter rules. Requires sideFiltersConfig.
sideFiltersConfigDocyrusDataGridSideFiltersConfig<TData>—Configuration for the side panel. Required when enableSideFilters is true. See Side panel filters.
sideFiltersDefaultExpandedbooleantrueInitial expanded state. When false, the panel mounts collapsed and renders a thin vertical rail with a 90°-rotated "Filters" label that re-expands the panel on click.
sideFiltersExpandedboolean—Controlled expanded state. Pair with onSideFiltersExpandedChange.
onSideFiltersExpandedChange(expanded: boolean) => void—Called when the user toggles the panel via the close icon (top right of the panel header) or the rotated rail button.
sideFiltersWidthnumber | string280Width of the side panel when expanded. Number → px.
onReload() => void—Called when the reload button is clicked, after the hook's internal refetch runs.
enableClientExportMenubooleanfalseAdds a client-side export dropdown to the toolbar (row scope: filtered / all / selected; column scope: visible / all). Only rendered when the server export menu is off, e.g. for local data sources.
defaultPagingMode'standard' | 'virtual-scroll''virtual-scroll'Paging mode used when the active view carries no paging setting. Opt into 'standard' when a data source exceeds the single-request row cap and users need a paging footer to reach the rest.
enableColumnReorderbooleanfalseLets users reorder columns: "Move left / right" items in the column menu plus drag-and-drop on the header row. System columns (select, actions) never move.
showDropdownChevronbooleanfalseDraws a small always-visible chevron on dropdown cells (select, status, multi-select, tags) so they read as editable before interaction. Hidden on read-only cells.
relationIconFieldsRecord<string, string>—Per-relation-column icon source. Maps each relation field slug to the slug of an icon-/image-typed field on the related data source. The hook expands the relation projection (<relationSlug>(id, name, autonumber_id, <iconFieldSlug>)) and RelationCell prefixes the name with the resolved <DocyrusIcon> (field-icon string) or <img> thumbnail (field-image value). See Relation cell icons / logos.
enableRelationColumnsbooleanfalseOffer columns borrowed from a related data source in the Fields menu — "the Matter's Project Lead next to each time entry". Costs one extra request per data source (GET /v1/dev/data-sources/:id/fields?expand=parent); the borrowed columns start hidden, so nothing changes until the user picks one. See Borrowed relation columns.
searchPlaceholderstring'Search…'Placeholder for the toolbar search input.
searchDebounceMsnumber300Debounce in ms before the search input is sent as filterKeyword.
toolbarClassNamestring—Extra className for the toolbar root.

Return Value

PropertyTypeDescription
tableTable<TData>TanStack Table instance — pass to <DataGrid> and any toolbar building block.
gridPropsOmit<UseDataGridResult, 'table'>Spread onto <DataGrid table={table} {...gridProps} />.
toolbarReactNodePre-wired toolbar element ready to render above the grid.
sidePanelReactNodeVertical view-picker panel. null for 'horizontal-tabs'/'dropdown' view variants; otherwise a <DataGridSidePanel> containing <DataGridViewSelect variant="vertical-tabs" />.
sideFiltersReactNodeSide-panel filter rail. null when enableSideFilters is false. Renders the full <DataTableSideFilters> panel (with a panel-close icon in the header) when expanded, and a thin vertical rail (with a 90°-rotated "Filters" label) when collapsed.
sideFiltersExpandedbooleanCurrent expanded state of the side filter panel.
setSideFiltersExpanded(expanded: boolean) => voidProgrammatically toggle the side filter panel.
sideFiltersQueryRuleGroupType | undefinedCurrent RuleGroupType emitted by the side panel — already merged into resolvedListParams.filters. Exposed for inspection / persistence.
pivotFilterRuleArray<PivotFilterRule> | nullCurrent AND-combined rule produced by the pivot filter strips (when pivotFilters is set). null when no pivot has a selection. Already merged into the items query — exposed for inspection (e.g. a "1 pivot filter active" badge).
itemsArray<TData>Resolved rows passed to the grid — from data, collection.list(), or the direct items fetch.
resolvedListParamsDocyrusDataGridListParamsThe list params actually sent to the backend (after merging view state, search, side panel filters, and listParams).
reload() => voidTriggers refetch of the data source, views, and items queries plus the optional onReload callback.
viewsSavedDataGridView[]Saved views mapped from the backend shape.
fieldsFullField[]Fields mapped for react-querybuilder filter editor.
dataSourceDataSource | undefinedRaw data source metadata response.
activeViewIdstringId of the currently active view.
setActiveViewId(viewId: string) => voidProgrammatically switch views. Persisted per user (follows persistState storage when enabled, localStorage otherwise).
isLoadingbooleantrue until all queries (data source, views, items) have resolved.
errorError | nullFirst error from any of the queries.
refetch() => voidAlias for reload.

Pivot filter integration

Pass pivotFilters: [{ fieldSlug: 'status' }, { fieldSlug: 'priority' }, …] and the hook stacks one or more pivot filter strips above the data-view toolbar automatically — no extra rendering or state-lifting required on the consumer side.

const grid = useDocyrusDataGrid({
  client,
  appSlug: 'base',
  dataSourceSlug: 'task',
  pivotFilters: [
    { fieldSlug: 'status', totalLabel: 'All statuses' },
    { fieldSlug: 'priority', totalLabel: 'All priorities' }
  ]
});

return (
  <div className="flex h-full flex-col gap-4">
    {grid.toolbar}        {/* pivot strips appear here, above the data-view toolbar */}
    <DataGrid table={grid.table} {...grid.gridProps} />
  </div>
);

What the hook does internally:

  • Renders <DocyrusPivotFilterGroup> at the top of the returned toolbar, so pivots show up above the existing controls without the consumer having to place them.
  • Owns the per-strip selection state via the child-component pattern (each pivot runs its own useDocyrusPivotFilter).
  • AND-merges the combined filterRule into resolvedListParams.filters alongside the saved view filter, toolbar filter, and side-panel filter — so pivot selections are applied to the items query.
  • Feeds the external filter chain (saved view + toolbar + side, without the pivot's own rule) back into each pivot as activeFilters so pill counts cross-react when the user changes those filters too.
  • Exposes the combined rule via pivotFilterRule on the return — useful for badges, persistence, or downstream pages that mirror the same scope.

Each pivotFilters[i] entry accepts the same shape as DocyrusPivotFilterGroupField:

FieldTypeDescription
fieldSlugstringSlug of the field to pivot on.
defaultDateBucketPivotFilterDateBucketInitial date bucket for field-date / field-dateTime.
calculationPivotFilterCalculation | nullInitial calculation. null is count of id.
totalLabelstringOverride the "All" pill label.
hideZeroValuesbooleanHide pills whose stat is 0 (except the selected one).
disableSettingsbooleanHide the gear popover for this strip.
defaultSelectedItemIdstring | nullInitial selection.

For pages that need pivot strips outside the grid's toolbar (e.g. above a calendar or map), reach for the lower-level useDocyrusPivotFilter or <DocyrusPivotFilterGroup> directly.

Side panel filters

When enableSideFilters is true, the hook renders a <DataTableSideFilters> rail alongside the grid. The panel emits a RuleGroupType whenever the user adjusts a filter; the hook ANDs it onto the saved view's filterQuery and any toolbar filter menu rules before sending the items request — so side filters, toolbar filters, and the saved view all stack.

The toolbar filter menu (enableFilterMenu) is not auto-hidden; both can be active at the same time.

Collapse / expand

  • sideFiltersDefaultExpanded controls the initial state. Default true.
  • When expanded, a panel-close icon (top right of the panel header) collapses it to a vertical rail.
  • When collapsed, the rail shows a 90°-rotated "Filters" label (with the active-filter count appended) that re-expands the panel on click.
  • For controlled collapse state, pass sideFiltersExpanded + onSideFiltersExpandedChange.

DocyrusDataGridSideFiltersConfig<TData>

FieldTypeDefaultDescription
columnsConfigReadonlyArray<ColumnConfig<TData>>—Required. Filter columns for the panel. Mirrors the columnsConfig accepted by useDataTableFilters / useDataTableSideFilters.
strategy'server' | 'client''server'Filter strategy. Server-paged grids should use 'server' so values surface as RuleGroupType rules; 'client' filters the in-memory dataset.
defaultsSideFilterDefaults—Per-column UI hints (collapsed, mode, showMoreThreshold, hidden, sticky, etc.).
sectionsReadonlyArray<SideFilterSectionGroup>—Optional grouping of filter sections into named blocks.
variant'default' | 'bordered' | 'compact''default'Visual variant for the panel container.
titleReactNode'Filters'Header title. Pass null to hide.
showActiveChipsbooleantrueShow the active-filter chip strip below the header.
showClearAllbooleantrueShow the "Clear all" button next to the title when any filter is active.
searchableboolean | stringfalseRender a search input above the sections that drives the first text-typed column (or a column id you pass explicitly).
clearAllLabelstring'Clear all'Override for the "Clear all" label.
clearLabelstring'Clear'Override for the per-section "Clear" label.
localeLocale'en'Locale forwarded to the underlying filter UI.
classNamestring—Extra className for the panel root.
collapseAriaLabelstring'Collapse filters'Override for the collapse-button accessible label.
expandAriaLabelstring'Expand filters'Override for the expand-button accessible label.
collapsedWidthnumber | string36Width of the collapsed rail. Number → px.

Usage

'use client';

import { useDocyrusAuth } from '@docyrus/signin';

import { DataGrid } from '@docyrus/ui/components/data-grid';
import { useDocyrusDataGrid } from '@docyrus/ui/library/hooks/use-docyrus-data-grid';
import { createColumnConfigHelper } from '@docyrus/ui/components/data-table-filter';

type OrganizationRow = { id: string; name: string; status: 'active' | 'archived' };

const dtf = createColumnConfigHelper<OrganizationRow>();

const sideFilterColumns = [
  dtf
    .text()
    .id('name')
    .accessor(row => row.name)
    .displayName('Name')
    .build(),
  dtf
    .option()
    .id('status')
    .accessor(row => row.status)
    .displayName('Status')
    .options([
      { value: 'active', label: 'Active' },
      { value: 'archived', label: 'Archived' }
    ])
    .build()
] as const;

export function OrganizationsGrid() {
  const { client } = useDocyrusAuth();

  if (!client) return null;

  const {
    table, gridProps, toolbar, sideFilters
  } = useDocyrusDataGrid<OrganizationRow>({
    client,
    appSlug: 'crm',
    dataSourceSlug: 'organization',
    enableSideFilters: true,
    sideFiltersDefaultExpanded: true,
    sideFiltersWidth: 280,
    sideFiltersConfig: {
      columnsConfig: sideFilterColumns,
      strategy: 'server',
      defaults: {
        status: { mode: 'inline-checkbox' }
      }
    }
  });

  return (
    <div className="flex h-full flex-col gap-3">
      {toolbar}
      <div className="flex flex-1 min-h-0">
        {sideFilters}
        <DataGrid table={table} {...gridProps} height="auto" className="flex-1" />
      </div>
    </div>
  );
}

Advanced AND/OR filtering

The toolbar chip bar can express exactly one rule per column, all ANDed, with no combinator control — so status = Active OR owner = me has nowhere to live in it. The advanced filter panel is the surface for that query: a full QueryBuilderDocyrus with OR, nesting and the complete operator vocabulary, without having to save a view.

It rides in the toolbar next to the filter chips (part of enableFilterMenu) and needs no extra option to turn on.

How it composes with the chips

The advanced group is ANDed with the chip bar, and the chips stay live and editable beside it. Nothing is converted between the two surfaces in either direction — chips → group would be redundant (the chips already reach the wire) and group → chips is provably lossy (one rule per column, no combinator, no nesting).

The cost of two live surfaces is that a chip can contradict an advanced rule and return zero rows. That is stated in words inside the panel rather than hidden, and the group also shows as a chip in the same row so it is never invisible.

It is a draft editor

Nothing is written to the grid until Apply, and Apply prunes the group first. This is not politeness about request volume: the shapes react-querybuilder produces mid-edit are hard errors on /items (a rule with no field yet, a between holding an empty string) or silent zero-row answers (a freshly added empty group). Writing on every keystroke would blank the grid while the user is still typing, with no error surfaced anywhere.

Limits: 10 rules and 2 levels of nesting (ADVANCED_FILTER_MAX_RULES / ADVANCED_FILTER_MAX_DEPTH).

Where the group is stored

Under a reserved columnFilters entry (__advanced_filter__), not a parallel state slice. columnFilters is already lifted into this hook, fed to TanStack as controlled state, reset on view switch, rehydrated from the saved view / per-user overlay / session cache, snapshotted by persistState and passed to the filter builder — a reserved entry inherits all six wirings for free.

It is safe to park there because ColumnFiltersState carries no schema constraint, TanStack's filtered row model skips an unknown column id silently, and the chip bar skips any id with no column type — so a client-side grid is unaffected and no phantom chip is ever rendered.

A rule on a borrowed relation column may not sit inside an OR group — the backend's join semantics would annihilate the whole OR. Such rules are pruned before the request. See Filtering a borrowed column is positive-only.

Field Type → Cell Variant Mapping

The hook maps every Docyrus field type to the matching DataGrid cell variant when generating columns from dataSource.fields:

Docyrus typecell.variant
field-text, field-codeshort-text
field-textarea, field-htmlEditor, field-docEditorlong-text
field-emailemail
field-phonephone
field-urlurl
field-colorcolor
field-iconicon
field-number, field-autonumber, field-identitynumber
field-moneycurrency (with currency, decimalPrecision, thousandSeparator from the field's format or flat currency / decimal_precision / thousand_separator)
field-currencycurrency-code
field-percentpercent (with decimalPrecision, thousandSeparator, symbolPosition from format)
field-ratingrating (with max from format.ratingMax or maxRating, icon from format.ratingIcon)
field-durationduration
field-datedate
field-dateRangedate-range
field-dateTimedatetime
field-timetime
field-checkboxcheckbox
field-switchswitch
field-statusstatus (with options; see Status cells)
field-enum, field-systemEnumenum (with appSlug, dataSourceSlug, fieldSlug, options)
field-select, field-radioGroupselect (with options)
field-multiSelectmulti-select (with options)
field-tagSelecttag-select (with options)
field-filefile
field-imageimage
anything elseshort-text

Enum option resolution

For enum-backed fields (field-status, field-enum, field-systemEnum, field-select, field-radioGroup, field-multiSelect, field-tagSelect), options are read from the field's enums array (returned by the expand=enums data source fetch) and fall back to options if enums is absent. Each option's slug becomes the cell value and name becomes the label; color is forwarded when present.

Status cells

field-status renders with StatusCell, not the plain select. Options may carry parent (two-level status → sub-status lists, shown as "Main › Sub"), isFinalOption (closing statuses get a check mark) and forceDescription / forceFollowupDate. When a real data source is bound, the hook wires tableMeta.onStatusUpdate to POST /v1/apps/{app}/data-sources/{ds}/status-update/{fieldSlug}; the cell then asks for the required note / follow-up date before saving and logs the activity in addition to writing the value through the normal cell update — the endpoint only records the activity, it does not change the row. Without onStatusUpdate (local data), the cell behaves like a select and never asks for details it could not persist.

Out of scope

field-userSelect, field-userMultiSelect, field-relation, and field-relatedField need extra metadata that is not part of the data source field response (user list, related data source id). They fall back to short-text — pass mapColumn to render a richer cell when your app has the extra info on hand.

Translating column headers

Column headers default to each field's schema label (field.name). Pass dynamicLabelTranslator to remap them from your own i18n dictionary — the hook calls it once per column and renders whatever string you return. Omit the prop and headers stay exactly as the schema defines them.

The function receives the raw label and returns the display text. Return the label unchanged for anything you don't want to translate:

// Simple dictionary
const labels: Record<string, string> = {
  Name: 'İsim',
  Status: 'Durum',
  Description: 'Açıklama'
};

const { table, gridProps, toolbar } = useDocyrusDataGrid({
  client,
  appSlug: 'base',
  dataSourceSlug: 'task',
  dynamicLabelTranslator: (label) => labels[label] ?? label
});
// With an i18n library (i18next, next-intl, …)
const { t } = useTranslation();

useDocyrusDataGrid({
  client,
  appSlug: 'base',
  dataSourceSlug: 'task',
  dynamicLabelTranslator: (label) => t(`fields.${label}`, label)
});

The translator runs after mapColumn, so it also translates whatever string header your override produced. Only string headers are touched — a custom header render function (JSX) is left untouched. The translated value flows through the header, the fields menu, the filter menu, and the export column labels. Memoize the function (useCallback / useMemo) so the columns don't rebuild on every render.

Translating enum options

dynamicLabelTranslator only touches field-level labels (headers). To translate the option labels inside enum-backed cells and filter dropdowns (field-select, field-status, field-radioGroup, field-enum, field-multiSelect, field-tagSelect), pass dynamicEnumOptionTranslator.

Enum options carry a stable, language-independent slug (the stored value) plus a display name. Key your translation by enums.<field.slug>.<option.slug> and translate only name — slug, color, and icon are preserved automatically:

const { t } = useTranslation();

useDocyrusDataGrid({
  client,
  appSlug: 'base',
  dataSourceSlug: 'task',
  dynamicEnumOptionTranslator: (option, field) =>
    t(`enums.${field.slug}.${option.slug}`, option.name)
});
// Simple dictionary keyed by option slug
const statusLabels: Record<string, string> = { open: 'Açık', done: 'Tamamlandı' };

useDocyrusDataGrid({
  client,
  appSlug: 'base',
  dataSourceSlug: 'task',
  dynamicEnumOptionTranslator: (option) =>
    option.slug ? statusLabels[option.slug] ?? option.name : option.name
});

The translation is applied once at the field level, so the same remapped option names feed the cell, the filter menu, and the row-group value renderer — you don't wire each surface separately. It only touches static enum options; user / relation columns aren't affected. Pass the same function to useDocyrusFormView to keep grid and form option labels consistent. Memoize it so the columns don't rebuild every render.

On this page