# rn-query-builder URL: /docs/native/docyrus/query-builder Docyrus query builder for React Native — backend operator catalog, per-operator value editors, async relation pickers and AND/OR/NOT groups that emit the same RuleGroupType JSON as the web builder. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/rn-query-builder ``` **Dependencies:** - [@react-querybuilder/core](https://www.npmjs.com/package/@react-querybuilder/core) - [jsonata](https://www.npmjs.com/package/jsonata) - [@shopify/flash-list](https://www.npmjs.com/package/@shopify/flash-list) - [react-native-reanimated](https://www.npmjs.com/package/react-native-reanimated) - [@react-native-community/datetimepicker](https://www.npmjs.com/package/@react-native-community/datetimepicker) - [tailwind-variants](https://www.npmjs.com/package/tailwind-variants) `QueryBuilderDocyrus` is the native counterpart of the web `QueryBuilderDocyrus`. It is built on the pure-TS [`@react-querybuilder/core`](https://www.npmjs.com/package/@react-querybuilder/core) (query tools + mutation policy — no DOM, no redux) and produces **exactly the same `RuleGroupType` JSON** as the web builder: Docyrus backend operator ids (`=`, `like`, `between`, `is one of`, `last_7_days`, `json_key_is`, `active_user`, …), the same value shapes, `valueSource: 'value'` on rules and `not` on groups. A query authored on a phone can be sent to `/items` filters or saved into a data view unchanged, and vice versa. Mobile idioms replace the web popovers: every rule is a card, field → operator → value are picked in bottom sheets (searchable, sectioned, paged), nested groups are indented, tinted per depth and collapsible, and long-press offers Move up / Move down when `draggable` is on. ## Usage ```tsx import { useState } from 'react'; import { QueryBuilderDocyrus, type DocyrusQBField, type RuleGroupType } from '@/components/docyrus-native/query-builder'; const fields: DocyrusQBField[] = [ { name: 'name', label: 'Name', fieldType: 'field-text' }, { name: 'amount', label: 'Amount', fieldType: 'field-money' }, { name: 'close_date', label: 'Close date', fieldType: 'field-date' }, { name: 'status', label: 'Status', fieldType: 'field-status', values: [ { name: 'uuid-open', label: 'Open', color: '#3b82f6' }, { name: 'uuid-won', label: 'Won', color: '#22c55e' } ] }, { name: 'is_active', label: 'Active', fieldType: 'field-switch' } ]; export function DealFilter() { const [query, setQuery] = useState({ combinator: 'and', rules: [] }); return ( ); } ``` An id-less controlled query (like the empty one above) is prepared once per prop identity, so pass back the object you receive from `onQueryChange` and rule ids stay stable. ### Field → operator → editor mapping Each field resolves to a Docyrus `FilterGroup` — from `filterGroup`, or from `fieldType` through `FIELD_TYPE_TO_FILTER_GROUP` (unknown types fall back to `ALPHA`). With `enrichFieldDefaults` (default), the group supplies the operator list and the per-operator value editor: | Operator(s) | Editor | Stored `value` | |-------------|--------|----------------| | No-value operators (`empty`, `not empty`, `true`, `false`, `today`, `this_week`, `last_7_days`, `active_user`, …) | none | untouched | | `between` | two inputs (date / datetime pickers for DATE / DATETIME) | `[from, to]` | | `weekdays` | multi-select sheet | `string[]` of weekday numbers (`'1'`…`'6'`, `'0'` = Sunday) | | `json_key_is` / `json_key_is_not` | key + value inputs | `[key, value]` | | `json_key_is_empty` / `json_key_is_not_empty` | key input | `key` | | `x_days_ago`, `in_last_x_days`, `weekday_is`, `monthday_is`, … | numeric input | numeric string | | `time_is`, `time_greater`, … | native time picker | `HH:mm` | | `is_other_field` / `is_not_other_field` | field picker (sheet of the other fields) | field name | | RELATION `=` / `<>` | searchable record picker that also accepts a free token (`#FIELD=…`, `{{…}}`) | id or token | | RELATION / LIST `is one of` / `is none of` | multi picker | `string[]` of ids | | OWNER `=` / `<>` with `asyncOptions` | searchable single picker | user id | | FOLLOWER / MULTISELECT | multi picker | `string[]` | | DATE / DATETIME / TIME scalar operators | native date / datetime / time picker | `YYYY-MM-DD` / `YYYY-MM-DDTHH:mm` / `HH:mm` (the HTML-input strings the web editor stores) | | NUMERIC | numeric keyboard | string (as on web) | | Everything else | text input | string | `operatorValues` swaps the option list per operator and `operatorValueEditorType` overrides the editor per operator — identical to web. ## Async option loading A relation or user column's options live in another data source (or the tenant roster) and cannot ship in `values`. Declare `asyncOptions` on the field — the same `QBAsyncOptions` object the web builder takes: ```tsx const fields: DocyrusQBField[] = [ { name: 'account', label: 'Account', fieldType: 'field-relation', asyncOptions: { load: async ({ search, page, pageSize, signal }) => { const res = await client.get('/v1/apps/base/account/items', { filterKeyword: search, limit: pageSize, offset: page * pageSize }, { signal }); return { items: res.data.map(toOption), hasMore: res.data.length === pageSize }; }, resolveByIds: async ({ ids, signal }) => { const res = await client.get('/v1/apps/base/account/items', { filters: idFilter(ids) }, { signal }); return res.data.map(toOption); } } } ]; ``` The picker debounces the search (`debounceMs`, default `300`), loads page `0` when the sheet opens and — unlike the web picker, which only loads the first page — **pages on scroll** through FlashList `onEndReached` while `hasMore` is `true`. `resolveByIds` is asked, once, for the ids the picker cannot name, so a rule reopened from storage shows labels instead of raw ids. A sticky label cache keeps every option already seen. ## Depth limit `maxDepth` follows react-querybuilder's `maxLevels` semantics, exactly as the web builder forwards it: a group at depth `d` (root = `0`) shows **Group** while `d < maxDepth`, so `maxDepth={1}` allows one level of nested groups below the root. Unset, `0` or negative = unlimited (the default — the old native default of `3` is gone). ## Converting a query to JSONata `useQuery2JsonataConverter` / `convertQueryToJsonata` are the same pure-TS converter the web form-action engine uses, so a query can be evaluated on-device: ```tsx import { useQuery2JsonataConverter } from '@/components/docyrus-native/query-builder'; const { expression, evaluate, isValid } = useQuery2JsonataConverter({ query, fields, context: { activeUserId: user.id } }); const matches = await evaluate(record); // relative dates recomputed against "now" per call ``` ## API Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `query` | `RuleGroupType` | — | Controlled query. Pass back what `onQueryChange` gave you. | | `defaultQuery` | `RuleGroupType` | empty `and` group | Uncontrolled initial query. | | `onQueryChange` | `(query: RuleGroupType) => void` | — | Fires with the full next query on every change. | | `fields` | `Array` | — | Field list (see `DocyrusQBField`). | | `enrichFieldDefaults` | `boolean` | `true` | Derive `operators`, `valueEditorType` and `inputType` from each field's filter group. | | `operators` | `FlexibleOptionList` | react-querybuilder defaults | Fallback operator list for fields that declare none (only reached with `enrichFieldDefaults={false}`). | | `combinators` | `FlexibleOptionList` | `AND` / `OR` | Group combinator options. | | `getOperators` | `(field, { fieldData }) => FlexibleOptionList \| null` | — | Per-field operator resolver. | | `getValueEditorType` | `(field, operator, { fieldData }) => ValueEditorType` | — | Per-rule editor resolver. | | `getInputType` | `(field, operator, { fieldData }) => InputType \| null` | — | Per-rule input-type resolver. | | `getValues` | `(field, operator, { fieldData }) => FlexibleOptionList \| null` | — | Per-rule option resolver. | | `getDefaultField` | `string \| ((fields) => string)` | first field | Field of a new rule. | | `getDefaultOperator` | `string \| ((field, { fieldData }) => string)` | first operator | Operator of a new rule. | | `getDefaultValue` | `(rule, { fieldData }) => unknown` | — | Value of a new rule. | | `onAddRule` | `(rule, parentPath, query, context?) => RuleType \| boolean` | — | Return `false` to cancel or a replacement rule. | | `onAddGroup` | `(group, parentPath, query, context?) => RuleGroupType \| boolean` | — | Return `false` to cancel or a replacement group. | | `onRemove` | `(ruleOrGroup, path, query, context?) => boolean` | — | Return `false` to cancel. | | `onMoveRule` | `(ruleOrGroup, oldPath, newPath, query, nextQuery) => RuleGroupType \| boolean` | — | Confirm / replace a rule move or clone. | | `onMoveGroup` | `(ruleOrGroup, oldPath, newPath, query, nextQuery) => RuleGroupType \| boolean` | — | Confirm / replace a group move or clone. | | `showNotToggle` | `boolean` | `false` | NOT switch on every group. | | `showCloneButtons` | `boolean` | `false` | Clone actions on rules and nested groups. | | `addRuleToNewGroups` | `boolean` | `false` | New groups start with one rule. | | `resetOnFieldChange` | `boolean` | `true` | Reset operator + value when the field changes. | | `resetOnOperatorChange` | `boolean` | `false` | Reset the value when the operator changes. | | `disabled` | `boolean \| Path[]` | `false` | Disable everything, or only the given paths (and their descendants). | | `translations` | `QBTranslations` | — | Label overrides (react-querybuilder `translations` shape). | | `idGenerator` | `() => string` | `generateID` | Id factory for new rules / groups. | | `maxDepth` | `number` | unlimited | Maximum group nesting (react-querybuilder `maxLevels` semantics). | | `variant` | `'default' \| 'bordered' \| 'compact' \| 'striped'` | `'default'` | Visual variant. | | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Density; `lg` uses medium-size inputs. | | `animated` | `boolean` | `true` | Reanimated enter / exit / layout animations. | | `draggable` | `boolean` | `false` | Long-press a rule or group for Move up / Move down. | | `showRuleNumbers` | `boolean` | `false` | Prefix each rule card with its index in its group. | | `emptyMessage` | `ReactNode` | — | Shown while the root group has no rules. | | `showClearButton` | `boolean` | `false` | "Clear All" button (confirmed by an `AlertDialog`) while rules exist. | | `clearButtonLabel` | `ReactNode` | `'Clear All'` | Clear button label. | | `onRuleAdd` | `() => void` | — | Fired before a rule is added. | | `onRuleRemove` | `() => void` | — | Fired before a rule is removed. | | `onGroupAdd` | `() => void` | — | Fired before a group is added. | | `onGroupRemove` | `() => void` | — | Fired before a group is removed. | | `onClear` | `() => void` | — | Fired when all rules are cleared. | | `showSummary` | `boolean` | `false` | Native only — condition / nested-group count chips in the root header. | | `collapsible` | `boolean` | `true` | Native only — collapse chevron on nested groups. | | `controlElements` | `Partial` | — | Replace individual controls (same slot names as web). | | `controlClassnames` | `Partial` | — | className overrides per structural slot. | | `className` | `string` | — | Root container className. | ### QBControlElements | Slot | Props | Default | |------|-------|---------| | `actionElement` | `ActionProps` | `QBActionElement` | | `addRuleAction` / `addGroupAction` | `ActionProps` | `QBActionElement` | | `removeRuleAction` / `removeGroupAction` | `ActionProps` | `QBActionElement` | | `cloneRuleAction` / `cloneGroupAction` | `ActionProps` | `QBActionElement` | | `fieldSelector` | `FieldSelectorProps` | `QBValueSelector` (use `QBGroupedFieldSelector` for relation-heavy sources) | | `operatorSelector` | `OperatorSelectorProps` | `QBValueSelector` | | `combinatorSelector` | `CombinatorSelectorProps` | `QBCombinatorSelector` | | `notToggle` | `NotToggleProps` | `QBNotToggle` | | `valueEditor` | `ValueEditorProps` | `QBValueEditor` | ### QBClassnames | Key | Applies to | |-----|-----------| | `queryBuilder` | Root container | | `ruleGroup` | Every group card | | `header` | Group header row | | `body` | Group children list | | `rule` | Every rule card | ### QBTranslations Every key is optional and takes `{ label?: string; title?: string }`: `fields` (+ `placeholderLabel`), `operators` (+ `placeholderLabel`), `value`, `addRule`, `addGroup`, `removeRule`, `removeGroup`, `cloneRule`, `cloneRuleGroup`, `combinators`, `notToggle`, `shiftActionUp`, `shiftActionDown`. Unset keys fall back to `useUiTranslation()` (`ui.queryBuilder.*`) and then English. ## Components | Component | Description | |-----------|-------------| | `QueryBuilderDocyrus` | The builder | | `QBActionElement` | Add / remove / clone button | | `QBValueSelector` | Bottom-sheet single / multi select (field, operator and generic selector) | | `QBGroupedFieldSelector` | Searchable field picker grouping `"Relation → Field"` labels under "via Relation" | | `QBCombinatorSelector` | AND / OR segmented control | | `QBValueEditor` | Per-operator value editor | | `QBNotToggle` | NOT switch | ## Hooks & functions | Export | Description | |--------|-------------| | `useQuery2JsonataConverter(options)` | Memoized query → JSONata predicate with `evaluate(record)` | | `convertQueryToJsonata(query, options)` | Pure converter | | `computeDateBindings` / `computeDateWindow` / `createHelperBindings` / `defaultFieldResolver` | Converter helpers | | `getFilterGroupForFieldType(fieldType)` | Docyrus field type → `FilterGroup` | | `getOperatorsForGroup(group)` | Operator list for a group | | `resolveValueEditorType(group, operator, fallback)` / `resolveInputType(group, operator, fallback)` | Per-operator editor / input type | | `OPERATOR_LABELS`, `OPERATORS_BY_GROUP`, `NO_VALUE_OPERATORS`, `BETWEEN_OPERATORS`, `NUMBER_VALUE_OPERATORS`, `TIME_VALUE_OPERATORS`, `WEEKDAYS_OPERATORS`, `OTHER_FIELD_OPERATORS`, `MULTI_VALUE_OPERATORS`, `ARRAY_VALUE_OPERATORS`, `JSON_KEY_OPERATORS`, `JSON_KEY_NO_VALUE_OPERATORS`, `FIELD_TYPE_TO_FILTER_GROUP`, `FILTER_GROUP_INPUT_TYPE`, `FILTER_GROUP_VALUE_EDITOR_TYPE`, `SERVER_BROKEN_RELATIVE_DATE_OPERATORS`, `WEEKDAY_OPTIONS` | Operator catalog (synced from web) | ## Type Exports | Type | Description | |------|-------------| | `QueryBuilderDocyrusProps` | Component props | | `DocyrusQBField` | Field with Docyrus extensions | | `QBFullField` | Field after enrichment (`name` + `value` guaranteed) | | `QBAsyncOptions` | Server-side option loading config | | `QBValueOption` | One option (`FlatOption`) | | `QBOperatorValueOption` | Entry of `operatorValues` | | `QBTranslation` / `QBTranslations` | Label overrides | | `QBControlElements` / `QBClassnames` | Customization slots | | `QBOnAddRule` / `QBOnAddGroup` / `QBOnRemove` / `QBOnMove` | Callback signatures | | `ActionProps` / `VersatileSelectorProps` / `FieldSelectorProps` / `OperatorSelectorProps` / `CombinatorSelectorProps` / `NotToggleProps` / `ValueEditorProps` | Control props | | `RuleGroupType` / `RuleGroupTypeIC` / `RuleGroupTypeAny` / `RuleType` / `Field` / `FullField` / `FullOperator` / `FullCombinator` | Re-exported from `@react-querybuilder/core` | | `QueryJsonataContext` / `EvaluateOptions` / `UseQuery2JsonataConverterOptions` / `UseQuery2JsonataConverterResult` | Converter hook types | | `ConvertQueryOptions` / `ConvertQueryResult` / `DateBindingSpec` / `OperatorApi` | Converter types | | `FilterGroup` | `'ALPHA' \| 'APPROVAL' \| 'BOOL' \| 'CODE' \| 'COMMON' \| 'DATE' \| 'DATETIME' \| 'FILE' \| 'FOLLOWER' \| 'JSON' \| 'LIST' \| 'MULTISELECT' \| 'NUMERIC' \| 'OWNER' \| 'RELATION' \| 'TIME'` | ## Type Reference ### DocyrusQBField Extends react-querybuilder's `Field` (`name`, `label`, `values`, `operators`, `valueEditorType`, `inputType`, `defaultOperator`, `defaultValue`, …). | Field | Type | Description | |-------|------|-------------| | `filterGroup` | `FilterGroup` | Explicit Docyrus filter group. | | `fieldType` | `string` | Docyrus field type (`field-text`, `field-relation`, …) resolved to a group. | | `asyncOptions` | `QBAsyncOptions` | Server-side option loading for relation / user columns. | | `operatorValues` | `Record` | Per-operator option lists. | | `operatorValueEditorType` | `Partial>` | Per-operator editor overrides. | ### QBAsyncOptions | Field | Type | Default | Description | |-------|------|---------|-------------| | `load` | `(params: { search, page, pageSize, signal? }) => Promise<{ items: QBValueOption[]; hasMore?: boolean }>` | — | Fetch one page. Aborted when superseded or the sheet closes. | | `resolveByIds` | `(params: { ids: string[]; signal? }) => Promise` | — | Labels for already-selected values. | | `pageSize` | `number` | `25` | Items per page. | | `debounceMs` | `number` | `300` | Debounce before a new search fires. | ### QBValueOption | Field | Type | Description | |-------|------|-------------| | `name` | `string` | Stored value (id). | | `label` | `string` | Display label. | | `icon` | `string` | Docyrus icon. | | `color` | `string` | Dot color. | | `secondaryLabel` | `string` | Disambiguator shown as `#…` (e.g. `autonumber_id`). | ## Migrating from the pre-parity native QueryBuilder The old hand-rolled `QueryBuilder` was not wire-compatible with Docyrus. It is replaced, not aliased: | Before | Now | |--------|-----| | `QueryBuilder` | `QueryBuilderDocyrus` | | `onChange` | `onQueryChange` | | `QueryField { id, label, type, options }` | `DocyrusQBField { name, label, fieldType \| filterGroup, values }` | | `QueryGroup` / `QueryRule` | `RuleGroupType` / `RuleType` (id optional, `not`, `valueSource`) | | operators `equals`, `not_equals`, `contains`, `gt`, `lt`, `between`, `in`, `is_empty` | backend ids `=`, `<>`, `like`, `>`, `<`, `between`, `is one of`, `empty` (per filter group) | | `maxDepth` default `3` | unlimited by default (same semantics) | | `style` | removed — use `className` |