Components

Query Builder

Query Builder component.

Client Only

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-query-builder
Required Packages(8 packages)
pnpm add @react-querybuilder/dnd @atlaskit/pragmatic-drag-and-drop @dnd-kit/core react-dnd react-dnd-html5-backend react-dnd-touch-backend react-querybuilder jsonata
UI Primitives(13 components)
npx shadcn@latest add alert-dialog badge button checkbox command input label popover radio-group select switch textarea toggle-group

Usage

import {
  QueryBuilderDocyrus
} from "@docyrus/ui/components/query-builder";
import type {
  QueryBuilderDocyrusProps,
  DocyrusQBField,
  RuleGroupType,
  RuleGroupTypeIC,
  RuleType,
  Field,
  FullField,
  FullOperator,
  FullCombinator,
  QueryBuilderProps,
  ActionProps,
  ValueSelectorProps,
  ValueEditorProps,
  NotToggleProps,
  DragHandleProps,
  CombinatorSelectorProps,
  FieldSelectorProps,
  OperatorSelectorProps,
  RuleGroupTypeAny,
  VersatileSelectorProps,
  QueryJsonataContext,
  EvaluateOptions,
  UseQuery2JsonataConverterOptions,
  UseQuery2JsonataConverterResult,
  ConvertQueryOptions,
  ConvertQueryResult,
  DateBindingSpec,
  OperatorApi,
  FilterGroup
} from "@docyrus/ui/components/query-builder";

<QueryBuilderDocyrus
  variant="default"
  size="sm"
  animated
  draggable
  showRuleNumbers
  maxDepth={0}
  emptyMessage={...}
  showClearButton
  clearButtonLabel={...}
  onRuleAdd={() => {}}
  onRuleRemove={() => {}}
  onGroupAdd={() => {}}
  onGroupRemove={() => {}}
  onClear={() => {}}
  enrichFieldDefaults
/>

Variants

VariantDescription
defaultDefault style
borderedBordered — rounded-lg border bg-card p-4 shadow-sm
compactCompact — text-xs [&_.rule]:py-1 [&_.rule]:px-2 [&_.rule]:gap-1
stripedStriped — [&_.rule:nth-child(even)]:bg-muted/20

Sizes

SizeDescription
smSmall — [&_.qb-control]:h-7 [&_.qb-control]:text-xs
defaultDefault
lgLarge — [&_.qb-control]:h-10 [&_.qb-control]:text-sm

API Reference

PropTypeDefault
variant"default" | "bordered" | "compact" | "striped""default"
size"sm" | "default" | "lg""default"
animatedboolean—
draggableboolean—
showRuleNumbersboolean—
maxDepthnumber—
emptyMessageReactNode—
showClearButtonboolean—
clearButtonLabelReactNode—
onRuleAdd() => void—
onRuleRemove() => void—
onGroupAdd() => void—
onGroupRemove() => void—
onClear() => void—
enrichFieldDefaultsboolean—
classNamestring—

Async option loading

A relation or user column has an option set too large to ship up front — the related data source's records, or the whole tenant roster. Declare asyncOptions on that field and its value editor switches to a searchable, paged picker that loads from the server:

const fields: Array<DocyrusQBField> = [
  {
    name: 'matter',
    label: 'Matter',
    fieldType: 'field-relation',
    asyncOptions: {
      load: async ({ search, page, pageSize, signal }) => {
        const res = await client.get('/v1/apps/base/matter/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/matter/items', { filters: idFilter(ids) }, { signal });

        return res.data.map(toOption);
      }
    }
  }
];

Opt-in per field: a field that declares no asyncOptions keeps the static values behaviour exactly as before, so existing consumers are untouched. The shape mirrors the data grid's AsyncOptionsConfig, so a host that already builds a loader for the grid's filter menu can reuse the same one.

Why resolveByIds matters

Without it, a rule reopened from storage prints raw ids. Options arrive one page at a time and only while the picker is open, so a record chosen yesterday is almost never in today's first page. The picker asks for exactly the ids it cannot name, once, and remembers the answer in a sticky label cache.

maxDepth now actually applies. It used to be forwarded as maxGroupLevel, which react-querybuilder has never read — so the cap was silently inert and a consumer asking for a depth limit got unlimited nesting. It is forwarded as maxLevels now, so a previously-ignored maxDepth starts enforcing itself.

Components

ComponentDescription
QueryBuilderDocyrusPublic export

Type Exports

TypeDescription
QueryBuilderDocyrusProps—
QBAsyncOptionsServer-side option loading config for one field
QBValueOptionOne option (FlatOption) returned by an async loader
DocyrusQBField—
RuleGroupType—
RuleGroupTypeIC—
RuleType—
Field—
FullField—
FullOperator—
FullCombinator—
QueryBuilderProps—
ActionProps—
ValueSelectorProps—
ValueEditorProps—
NotToggleProps—
DragHandleProps—
CombinatorSelectorProps—
FieldSelectorProps—
OperatorSelectorProps—
RuleGroupTypeAny—
VersatileSelectorProps—
QueryJsonataContext—
EvaluateOptions—
UseQuery2JsonataConverterOptions—
UseQuery2JsonataConverterResult—
ConvertQueryOptions—
ConvertQueryResult—
DateBindingSpec—
OperatorApi—
FilterGroup—

Type Reference

DocyrusQBField

FieldTypeDescription
filterGroupFilterGroup—
fieldTypestring—
operatorValuesRecord<string, QBOperatorValueOption[]>—
operatorValueEditorTypePartial<Record<string, ValueEditorType>>—
asyncOptionsQBAsyncOptionsServer-side option loading for this field. Set it for a relation or user column whose options cannot be shipped in values.

QBAsyncOptions

FieldTypeDefaultDescription
load(params: { search, page, pageSize, signal? }) => Promise<{ items: Array<QBValueOption>; hasMore?: boolean }>—Fetch one page of options. Aborted when the editor unmounts or a fresh search supersedes it.
resolveByIds(params: { ids: Array<string>; signal? }) => Promise<Array<QBValueOption>>—Labels for values that are already selected, so a rule reopened from storage doesn't print raw ids.
pageSizenumber25Items per page.
debounceMsnumber300Debounce before a new search fires.

QueryJsonataContext

FieldTypeDescription
activeUserIdstring—
activeUserTeamMemberIdsstring[]—

EvaluateOptions

FieldTypeDescription
nowDate | number—
bindingsRecord<string, unknown>—

UseQuery2JsonataConverterOptions

FieldTypeDescription

UseQuery2JsonataConverterResult

FieldTypeDescription
expressionstring—
dateBindingsDateBindingSpec[]—
unsupportedOperatorsstring[]—
warningsstring[]—
isValidboolean—
errorError | null—
evaluate(data: unknown, options?: EvaluateOptions) => Promise<boolean>—

ConvertQueryOptions

FieldTypeDescription
resolveGroup(field: string) => FilterGroup | undefined—
fieldResolver(field: string) => string—
operatorOverridesRecord<string, (rule: RuleType, api: OperatorApi) => string | null>—
unsupportedFallbackboolean—
weekStartsOn0 | 1—

ConvertQueryResult

FieldTypeDescription
expressionstring—
dateBindingsDateBindingSpec[]—
unsupportedOperatorsstring[]—
warningsstring[]—

DateBindingSpec

FieldTypeDescription
idstring—
operatorstring—
amountnumber—

OperatorApi

FieldTypeDescription
fieldstring—
str(value: unknown) => string—
num(value: unknown) => string—
strArr(values: unknown[]) => string—
fieldRef(name: string) => string—

FilterGroup

"ALPHA"

On this page