QueryBuilder
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
pnpm dlx @docyrus/cli add @docyrus/rn-query-builderpnpm add @react-querybuilder/core jsonata @shopify/flash-list react-native-reanimated @react-native-community/datetimepicker tailwind-variantsQueryBuilderDocyrus is the native counterpart of the web QueryBuilderDocyrus. It is built on the pure-TS @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
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<RuleGroupType>({ combinator: 'and', rules: [] });
return (
<QueryBuilderDocyrus
fields={fields}
query={query}
onQueryChange={setQuery}
showNotToggle
showCloneButtons
maxDepth={3}
emptyMessage="No conditions yet. Add one to combine fields with AND / OR." />
);
}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:
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:
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 callAPI 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<DocyrusQBField | Field> | — | Field list (see DocyrusQBField). |
enrichFieldDefaults | boolean | true | Derive operators, valueEditorType and inputType from each field's filter group. |
operators | FlexibleOptionList<FullOperator> | react-querybuilder defaults | Fallback operator list for fields that declare none (only reached with enrichFieldDefaults={false}). |
combinators | FlexibleOptionList<FullCombinator> | AND / OR | Group combinator options. |
getOperators | (field, { fieldData }) => FlexibleOptionList<FullOperator> | 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<FullOption> | 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<QBControlElements> | — | Replace individual controls (same slot names as web). |
controlClassnames | Partial<QBClassnames> | — | 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<string, QBOperatorValueOption[]> | Per-operator option lists. |
operatorValueEditorType | Partial<Record<string, ValueEditorType>> | 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<QBValueOption[]> | — | 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 |