Docyrus

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.

iOSAndroid
Preview QueryBuilder on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-query-builder
Required Packages(6 packages)
pnpm add @react-querybuilder/core jsonata @shopify/flash-list react-native-reanimated @react-native-community/datetimepicker tailwind-variants

QueryBuilderDocyrus 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)EditorStored value
No-value operators (empty, not empty, true, false, today, this_week, last_7_days, active_user, …)noneuntouched
betweentwo inputs (date / datetime pickers for DATE / DATETIME)[from, to]
weekdaysmulti-select sheetstring[] of weekday numbers ('1'…'6', '0' = Sunday)
json_key_is / json_key_is_notkey + value inputs[key, value]
json_key_is_empty / json_key_is_not_emptykey inputkey
x_days_ago, in_last_x_days, weekday_is, monthday_is, …numeric inputnumeric string
time_is, time_greater, …native time pickerHH:mm
is_other_field / is_not_other_fieldfield 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 ofmulti pickerstring[] of ids
OWNER = / <> with asyncOptionssearchable single pickeruser id
FOLLOWER / MULTISELECTmulti pickerstring[]
DATE / DATETIME / TIME scalar operatorsnative date / datetime / time pickerYYYY-MM-DD / YYYY-MM-DDTHH:mm / HH:mm (the HTML-input strings the web editor stores)
NUMERICnumeric keyboardstring (as on web)
Everything elsetext inputstring

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 call

API Reference

PropTypeDefaultDescription
queryRuleGroupType—Controlled query. Pass back what onQueryChange gave you.
defaultQueryRuleGroupTypeempty and groupUncontrolled initial query.
onQueryChange(query: RuleGroupType) => void—Fires with the full next query on every change.
fieldsArray<DocyrusQBField | Field>—Field list (see DocyrusQBField).
enrichFieldDefaultsbooleantrueDerive operators, valueEditorType and inputType from each field's filter group.
operatorsFlexibleOptionList<FullOperator>react-querybuilder defaultsFallback operator list for fields that declare none (only reached with enrichFieldDefaults={false}).
combinatorsFlexibleOptionList<FullCombinator>AND / ORGroup 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.
getDefaultFieldstring | ((fields) => string)first fieldField of a new rule.
getDefaultOperatorstring | ((field, { fieldData }) => string)first operatorOperator 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.
showNotTogglebooleanfalseNOT switch on every group.
showCloneButtonsbooleanfalseClone actions on rules and nested groups.
addRuleToNewGroupsbooleanfalseNew groups start with one rule.
resetOnFieldChangebooleantrueReset operator + value when the field changes.
resetOnOperatorChangebooleanfalseReset the value when the operator changes.
disabledboolean | Path[]falseDisable everything, or only the given paths (and their descendants).
translationsQBTranslations—Label overrides (react-querybuilder translations shape).
idGenerator() => stringgenerateIDId factory for new rules / groups.
maxDepthnumberunlimitedMaximum 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.
animatedbooleantrueReanimated enter / exit / layout animations.
draggablebooleanfalseLong-press a rule or group for Move up / Move down.
showRuleNumbersbooleanfalsePrefix each rule card with its index in its group.
emptyMessageReactNode—Shown while the root group has no rules.
showClearButtonbooleanfalse"Clear All" button (confirmed by an AlertDialog) while rules exist.
clearButtonLabelReactNode'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.
showSummarybooleanfalseNative only — condition / nested-group count chips in the root header.
collapsiblebooleantrueNative only — collapse chevron on nested groups.
controlElementsPartial<QBControlElements>—Replace individual controls (same slot names as web).
controlClassnamesPartial<QBClassnames>—className overrides per structural slot.
classNamestring—Root container className.

QBControlElements

SlotPropsDefault
actionElementActionPropsQBActionElement
addRuleAction / addGroupActionActionPropsQBActionElement
removeRuleAction / removeGroupActionActionPropsQBActionElement
cloneRuleAction / cloneGroupActionActionPropsQBActionElement
fieldSelectorFieldSelectorPropsQBValueSelector (use QBGroupedFieldSelector for relation-heavy sources)
operatorSelectorOperatorSelectorPropsQBValueSelector
combinatorSelectorCombinatorSelectorPropsQBCombinatorSelector
notToggleNotTogglePropsQBNotToggle
valueEditorValueEditorPropsQBValueEditor

QBClassnames

KeyApplies to
queryBuilderRoot container
ruleGroupEvery group card
headerGroup header row
bodyGroup children list
ruleEvery 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

ComponentDescription
QueryBuilderDocyrusThe builder
QBActionElementAdd / remove / clone button
QBValueSelectorBottom-sheet single / multi select (field, operator and generic selector)
QBGroupedFieldSelectorSearchable field picker grouping "Relation → Field" labels under "via Relation"
QBCombinatorSelectorAND / OR segmented control
QBValueEditorPer-operator value editor
QBNotToggleNOT switch

Hooks & functions

ExportDescription
useQuery2JsonataConverter(options)Memoized query → JSONata predicate with evaluate(record)
convertQueryToJsonata(query, options)Pure converter
computeDateBindings / computeDateWindow / createHelperBindings / defaultFieldResolverConverter 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_OPTIONSOperator catalog (synced from web)

Type Exports

TypeDescription
QueryBuilderDocyrusPropsComponent props
DocyrusQBFieldField with Docyrus extensions
QBFullFieldField after enrichment (name + value guaranteed)
QBAsyncOptionsServer-side option loading config
QBValueOptionOne option (FlatOption)
QBOperatorValueOptionEntry of operatorValues
QBTranslation / QBTranslationsLabel overrides
QBControlElements / QBClassnamesCustomization slots
QBOnAddRule / QBOnAddGroup / QBOnRemove / QBOnMoveCallback signatures
ActionProps / VersatileSelectorProps / FieldSelectorProps / OperatorSelectorProps / CombinatorSelectorProps / NotToggleProps / ValueEditorPropsControl props
RuleGroupType / RuleGroupTypeIC / RuleGroupTypeAny / RuleType / Field / FullField / FullOperator / FullCombinatorRe-exported from @react-querybuilder/core
QueryJsonataContext / EvaluateOptions / UseQuery2JsonataConverterOptions / UseQuery2JsonataConverterResultConverter hook types
ConvertQueryOptions / ConvertQueryResult / DateBindingSpec / OperatorApiConverter 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, …).

FieldTypeDescription
filterGroupFilterGroupExplicit Docyrus filter group.
fieldTypestringDocyrus field type (field-text, field-relation, …) resolved to a group.
asyncOptionsQBAsyncOptionsServer-side option loading for relation / user columns.
operatorValuesRecord<string, QBOperatorValueOption[]>Per-operator option lists.
operatorValueEditorTypePartial<Record<string, ValueEditorType>>Per-operator editor overrides.

QBAsyncOptions

FieldTypeDefaultDescription
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.
pageSizenumber25Items per page.
debounceMsnumber300Debounce before a new search fires.

QBValueOption

FieldTypeDescription
namestringStored value (id).
labelstringDisplay label.
iconstringDocyrus icon.
colorstringDot color.
secondaryLabelstringDisambiguator 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:

BeforeNow
QueryBuilderQueryBuilderDocyrus
onChangeonQueryChange
QueryField { id, label, type, options }DocyrusQBField { name, label, fieldType | filterGroup, values }
QueryGroup / QueryRuleRuleGroupType / RuleType (id optional, not, valueSource)
operators equals, not_equals, contains, gt, lt, between, in, is_emptybackend ids =, <>, like, >, <, between, is one of, empty (per filter group)
maxDepth default 3unlimited by default (same semantics)
styleremoved — use className

On this page