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-gridpnpm add @docyrus/app-utils @docyrus/api-client @tanstack/react-query @tanstack/react-table react-querybuilderThis 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 throughuseDocyrusDataViewSelect), 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):
data— a pre-resolved array. Use when records come from somewhere outside of TanStack Query.collection— a TanStack DB collection generated by@docyrus/tanstack-db-generator(e.g., the result ofuseBaseOrganizationCollection()). The hook callscollection.list(resolvedListParams)inside its ownuseQuery.- Default —
GET /v1/apps/:appSlug/data-sources/:dataSourceSlug/itemsvia the@docyrus/api-clientyou already passed asclient. 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):
- Explicit prop on
useDocyrusDataGrid({ formatDate, formatDateTime, formatNumber })— wins when supplied. - Surrounding
useDateFormat()/useNumberFormat()context (typically installed by<DocyrusTenantProvider>). - The cell's own legacy fallback (
Intl.NumberFormatfor currency / percent inNumberCell, 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:
| Purpose | Token |
|---|---|
| Column id + projection | matter(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 theFROMclause. It narrows the row set before the boolean structure of theWHEREtree 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 containsis worse — itsis 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 param | Source | Notes |
|---|---|---|
columns | activeView.columnVisibility + dataSource.fields | Comma-separated slugs of all visible fields, with id always first. Omitted if no fields are loaded yet. |
orderBy | activeView.sorting (TanStack ColumnSort[]) | Mapped to [{ field, direction: 'asc' | 'desc' }, …]. Omitted when empty. |
filters | activeView.filterQuery + side panel RuleGroupType + toolbar filter rules | All sources are ANDed together. The shape matches IQueryFilterGroup. Omitted when no source produces any rules. See Side panel filters. |
filterKeyword | toolbar search input (debounced) | Trimmed; omitted when empty. |
limit, offset | defaultLimit option (default 100) + 0 | Override via listParams. |
| relation projection | visible 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 / filter | borrowed column sort + filter rules | Sort sends matter.project_lead; filters send rel_matter/project_lead, AND-only and positive-operator-only. |
| advanced filter group | toolbar advanced filter panel | ANDed with the chip-bar rules under a reserved columnFilters entry. See Advanced AND/OR filtering. |
| anything else | listParams | Spread 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.
| Option | Type | Default | Description |
|---|---|---|---|
data | Array<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. |
listParams | DocyrusDataGridListParams | — | 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). |
pivotFilters | ReadonlyArray<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. |
defaultLimit | number | 100 | Default page size when no limit is supplied via listParams. |
enableItemsQuery | boolean | true when no data | Toggle the internal items query. |
showSelectColumn | boolean | true | Show the row-select checkbox column as the first column. |
enableRowMarkers | boolean | true | Show 1-based row markers on the select column when not selected. |
selectColumn | ColumnDef<TData> | getDataGridSelectColumn({ size: 44, minSize: 44, enableRowMarkers }) | Override the default select column entirely. |
actionsColumn | ColumnDef<TData> | — | Optional actions column rendered as the second column (right after select). Build it with getDataGridActionsColumn({ cell: … }). |
extraColumns | Array<ColumnDef<TData>> | — | Extra columns prepended after the select + actions columns. |
defaultRowGroupingColumn | string | — | 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. |
enableSearch | boolean | false | Forwarded to useDataGrid (enables Cmd+F floating search inside the grid). |
enableGrouping | boolean | true | Forwarded to useDataGrid. Required for the row-grouping picker to populate; only override if you don't want grouping at all. |
readOnly | boolean | true | Forwarded to useDataGrid. |
enableViewSelect / enableSearchInput / enableFilterMenu / enableGroupMenu / enableSortMenu / enableRowHeightMenu / enableFieldsMenu / enableDisplayMenu / enableReloadButton | boolean | true | Toggle 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). |
enableSideFilters | boolean | false | Activate 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. |
sideFiltersConfig | DocyrusDataGridSideFiltersConfig<TData> | — | Configuration for the side panel. Required when enableSideFilters is true. See Side panel filters. |
sideFiltersDefaultExpanded | boolean | true | Initial 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. |
sideFiltersExpanded | boolean | — | 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. |
sideFiltersWidth | number | string | 280 | Width of the side panel when expanded. Number → px. |
onReload | () => void | — | Called when the reload button is clicked, after the hook's internal refetch runs. |
enableClientExportMenu | boolean | false | Adds 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. |
enableColumnReorder | boolean | false | Lets 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. |
showDropdownChevron | boolean | false | Draws 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. |
relationIconFields | Record<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. |
enableRelationColumns | boolean | false | Offer 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. |
searchPlaceholder | string | 'Search…' | Placeholder for the toolbar search input. |
searchDebounceMs | number | 300 | Debounce in ms before the search input is sent as filterKeyword. |
toolbarClassName | string | — | Extra className for the toolbar root. |
Return Value
| Property | Type | Description |
|---|---|---|
table | Table<TData> | TanStack Table instance — pass to <DataGrid> and any toolbar building block. |
gridProps | Omit<UseDataGridResult, 'table'> | Spread onto <DataGrid table={table} {...gridProps} />. |
toolbar | ReactNode | Pre-wired toolbar element ready to render above the grid. |
sidePanel | ReactNode | Vertical view-picker panel. null for 'horizontal-tabs'/'dropdown' view variants; otherwise a <DataGridSidePanel> containing <DataGridViewSelect variant="vertical-tabs" />. |
sideFilters | ReactNode | Side-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. |
sideFiltersExpanded | boolean | Current expanded state of the side filter panel. |
setSideFiltersExpanded | (expanded: boolean) => void | Programmatically toggle the side filter panel. |
sideFiltersQuery | RuleGroupType | undefined | Current RuleGroupType emitted by the side panel — already merged into resolvedListParams.filters. Exposed for inspection / persistence. |
pivotFilterRule | Array<PivotFilterRule> | null | Current 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). |
items | Array<TData> | Resolved rows passed to the grid — from data, collection.list(), or the direct items fetch. |
resolvedListParams | DocyrusDataGridListParams | The list params actually sent to the backend (after merging view state, search, side panel filters, and listParams). |
reload | () => void | Triggers refetch of the data source, views, and items queries plus the optional onReload callback. |
views | SavedDataGridView[] | Saved views mapped from the backend shape. |
fields | FullField[] | Fields mapped for react-querybuilder filter editor. |
dataSource | DataSource | undefined | Raw data source metadata response. |
activeViewId | string | Id of the currently active view. |
setActiveViewId | (viewId: string) => void | Programmatically switch views. Persisted per user (follows persistState storage when enabled, localStorage otherwise). |
isLoading | boolean | true until all queries (data source, views, items) have resolved. |
error | Error | null | First error from any of the queries. |
refetch | () => void | Alias 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 returnedtoolbar, 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
filterRuleintoresolvedListParams.filtersalongside 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
activeFiltersso pill counts cross-react when the user changes those filters too. - Exposes the combined rule via
pivotFilterRuleon the return — useful for badges, persistence, or downstream pages that mirror the same scope.
Each pivotFilters[i] entry accepts the same shape as DocyrusPivotFilterGroupField:
| Field | Type | Description |
|---|---|---|
fieldSlug | string | Slug of the field to pivot on. |
defaultDateBucket | PivotFilterDateBucket | Initial date bucket for field-date / field-dateTime. |
calculation | PivotFilterCalculation | null | Initial calculation. null is count of id. |
totalLabel | string | Override the "All" pill label. |
hideZeroValues | boolean | Hide pills whose stat is 0 (except the selected one). |
disableSettings | boolean | Hide the gear popover for this strip. |
defaultSelectedItemId | string | null | Initial 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
sideFiltersDefaultExpandedcontrols the initial state. Defaulttrue.- 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>
| Field | Type | Default | Description |
|---|---|---|---|
columnsConfig | ReadonlyArray<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. |
defaults | SideFilterDefaults | — | Per-column UI hints (collapsed, mode, showMoreThreshold, hidden, sticky, etc.). |
sections | ReadonlyArray<SideFilterSectionGroup> | — | Optional grouping of filter sections into named blocks. |
variant | 'default' | 'bordered' | 'compact' | 'default' | Visual variant for the panel container. |
title | ReactNode | 'Filters' | Header title. Pass null to hide. |
showActiveChips | boolean | true | Show the active-filter chip strip below the header. |
showClearAll | boolean | true | Show the "Clear all" button next to the title when any filter is active. |
searchable | boolean | string | false | Render a search input above the sections that drives the first text-typed column (or a column id you pass explicitly). |
clearAllLabel | string | 'Clear all' | Override for the "Clear all" label. |
clearLabel | string | 'Clear' | Override for the per-section "Clear" label. |
locale | Locale | 'en' | Locale forwarded to the underlying filter UI. |
className | string | — | Extra className for the panel root. |
collapseAriaLabel | string | 'Collapse filters' | Override for the collapse-button accessible label. |
expandAriaLabel | string | 'Expand filters' | Override for the expand-button accessible label. |
collapsedWidth | number | string | 36 | Width 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 type | cell.variant |
|---|---|
field-text, field-code | short-text |
field-textarea, field-htmlEditor, field-docEditor | long-text |
field-email | email |
field-phone | phone |
field-url | url |
field-color | color |
field-icon | icon |
field-number, field-autonumber, field-identity | number |
field-money | currency (with currency, decimalPrecision, thousandSeparator from the field's format or flat currency / decimal_precision / thousand_separator) |
field-currency | currency-code |
field-percent | percent (with decimalPrecision, thousandSeparator, symbolPosition from format) |
field-rating | rating (with max from format.ratingMax or maxRating, icon from format.ratingIcon) |
field-duration | duration |
field-date | date |
field-dateRange | date-range |
field-dateTime | datetime |
field-time | time |
field-checkbox | checkbox |
field-switch | switch |
field-status | status (with options; see Status cells) |
field-enum, field-systemEnum | enum (with appSlug, dataSourceSlug, fieldSlug, options) |
field-select, field-radioGroup | select (with options) |
field-multiSelect | multi-select (with options) |
field-tagSelect | tag-select (with options) |
field-file | file |
field-image | image |
| anything else | short-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.
useDocyrusDataGallery
One-call wiring of a Docyrus data source to a fully configured DataGallery + toolbar (DataGridViewSelect, search, filters, sort, group, gallery display menu, card config menu) — including row fetching, saved views, pivot filters, and automatic card field detection.
useDocyrusDataImportWizard
One-call wiring of a Docyrus data source to the DataImportWizard — handles upload, analyse, mapping, preview, and import in a single guided flow.