Skip to content

FilterEditor

Edits a filter tree. A row is a field, an operator and a value; groups nest under All or Any. The field is chosen with FieldPicker, so a row may filter through a link on a dotted path, and the value control comes from the field’s data type. The value is a FilterGroup, which toApi3Hash serialises to the body _search takes.

Terminal window
pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/filter-editor.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/filter-editor.json
FilterEditor: a catalogue of Version filters, one group per data type, with its serialised filter, a Note read-state row, and sizes

React

Loading the React demo…

Svelte

Loading the Svelte demo…

The first tree is a catalogue. One group holds each data type’s family – text, the numeric types, links, the coded lists, dates, and the flags and colours – and each group spreads that family’s operators, so every value control and every operator shape is on the page at once. The top level is Any, so the count under it counts rows.

A row is one 36px line: the field, the operator, the value and, on its own axis at the end, the remove control. A group is a header, its rows and a foot. The header carries the All and Any toggle and, on a nested group, the control that removes it; the rows hang off one rail, so a level of nesting reads as one indent; the foot carries the add-condition and add-group controls.

proptypedefaultmeaning
entityTypestringrequiredType the root of every field path is read on.
contextSgContextrequiredThe widget context. Every read goes through it, so widgets on a page share one cache.
valueFilterGrouprequiredThe tree. Two-way in Svelte through bind:value.
hidePathsstring[][]Paths kept out of the field list. A pattern hides itself and everything under it.
projectIdnumber—Scopes the status pickers to the codes one project allows.
emptyLabelstring'Nothing chosen'Shown when a group holds no condition.
disabledbooleanfalseDims the editor and blocks every control.
size'sm' | 'md' | 'lg''md'The control ladder: every control in a row stands at 28, 32 or 36, with a shadcn button of the same size.
class / classNamestring—Merged after the widget's own classes.

Every other attribute is spread onto the root: id, aria-*, data-*, key handlers and a ref to the root element.

eventpayloadwhen
onValueChangeFilterGroupThe whole tree changed, on every edit. Svelte also binds it with bind:value.

The operator picks the arity and the field’s data type picks the control inside it.

data type editor
number, float, percent, duration, timecode, currency NumberEditor
date DateEditor
date_time DateTimeEditor
list ListPicker on one value, ListMultiPicker on is any of and is none of
status_list StatusPicker on one value, StatusMultiPicker on is any of and is none of
checkbox CheckboxEditor
color ColorEditor
url UrlEditor
text, and any field under name is, name contains, type is TextEditor
entity, multi_entity EntityPicker on one value, EntityMultiPicker on is any of and is none of

Inside a row the date, date-time, number and colour editors take their inline form: the width the data type needs, and no zone line under a date-time. A date and a date-time are the same picker button here as anywhere else, with the calendar in its popover. Between draws both ends on one line.

Arity comes first. An operator that carries its own value — is empty, is not empty, today, this week, last month — draws no editor. In the last and in the next draw a count in a NumberEditor and a unit in a ListPicker. Between draws two of the data type’s editor. Is any of and is none of on a date, a number or a text field draw one value to a line, each in the data type’s own editor, with a control that removes it and one that adds another; a blank value is dropped on serialisation rather than sent.

A duration is typed as 1h 30m, 1:30 or 90 and stored as the 90 minutes all three mean.

slotreceives
fieldChooserentityType, the current path, hidePaths, filterableOnly, disabled, onSelect(path).
valueEditorfield, dataType, operator, value, arity, disabled, onChange(value).
entityEditorThe same arguments as the value editor.

The field chooser is FieldPicker unless the slot replaces it. The value editor slot replaces every control in the table above; the entity slot replaces only the two entity pickers, for a caller that wants its own row or its own search.

keydoes
TabMoves between field, operator, value and the remove control of each row.
Enter, SpaceOpens the field chooser, the operator menu or a value popover.
Arrow keysMove the cursor in an open menu or list; move between All and Any.
Right arrowDescends into the highlighted link in the field chooser.
Left arrowGoes back one level in the field chooser.
EnterChooses the highlighted field, operator or value. In a multi-value list it checks the highlighted entry and the list stays open.
EscapeCloses the open menu or popover.
BackspaceIn an entity picker's empty search box, takes the caret to the last chip; a second press removes it.

The menu shows wording, not wire names. Every entry names one raw operator.

menu entry operator value sent
is is one value of the field’s kind
is not is_not one value
is any of in a list
is none of not_in a list
contains, does not contain, starts with, ends with contains, not_contains, starts_with, ends_with one string
greater than / after greater_than one value, strict
less than / before less_than one value, strict
between between two values, inclusive both ends
in the last, not in the last in_last, not_in_last [count, UNIT], count positive
in the next, not in the next in_next, not_in_next [count, UNIT]
today, yesterday, tomorrow in_calendar_day 0, -1, 1
this week, last week, next week in_calendar_week 0, -1, 1
this month, last month, next month in_calendar_month 0, -1, 1
this year, last year, next year in_calendar_year 0, -1, 1
is empty is null
is not empty is_not null
name is, name contains, name does not contain name_is, name_contains, name_not_contains one string
type is, type is not type_is, type_is_not a type name

Which entries appear is decided by the field’s data_type.

The operator vocabulary is per data type, and a wrong operator is a 400 naming the legal set (017_filter_operators). Five data types take no filter operator at all — url, calculated, password, serializable and summary — and answer data type cannot be used in a filter with no legal set to name. A pivot_column advertises is and is_not and rejects both (field_types/pivot_column). All six are left out of the field list.

Groups serialise to {logical_operator, conditions} with both keys required, and and or in lowercase and nothing else, and no negation operator; nesting is safe to 265 levels (030_complex_filters). A group with no conditions matches every row, so an empty tree serialises to null rather than to an empty filter.

in_calendar_day, _week, _month and _year take a signed offset from the current bucket, 0 being this one, not a count (field_types/date). in_last and in_next take [count, UNIT] with a positive integer and an uppercase unit out of HOUR, DAY, WEEK, MONTH, YEAR; a window with no count sends 1, the smallest the API takes. between is inclusive at both ends and order-insensitive.

A colour is the decimal r,g,b the field stores, with no spaces and no #; hex is rejected inside a filter as it is on write (field_types/color).

An entity value is a {type, id} hash under is and a list of them under in; a list under is is a 400 (field_types/entity). A status or list value is the raw code, never the display label (field_types/status_list).

Every negating operator — is_not, not_in, not_contains, not_in_last, not_in_next, name_not_contains, type_is_not — also matches rows where the field is unset, while greater_than and less_than exclude them. To mean “has a value and is not X”, add a second is not empty row.

A checkbox is two-state and never null, so it has no empty test: is null on one is a 400 (field_types/checkbox). An image field takes null and nothing else, so its only entries are the empty pair (field_types/image). A uuid field spells its empty test is "", which this editor reads as an unfilled row, so it offers no empty entry there (field_types/uuid).

Dates are YYYY-MM-DD and date-times YYYY-MM-DDTHH:MM:SSZ, always UTC; an epoch integer and a slashed date are both 400s (field_types/date, field_types/date_time).

A dotted path filters through a link, and the middle segment names the type it travels through: entity.Shot.sg_sequence reads Shot from Version’s Link field. Only a single entity field is descended into; a path through a multi_entity field reads back nothing, 200 with the key absent (probe 016).

The row and group anatomy follows ReUI’s Filters 2.5.2: the field, operator and value cells on one band and the row’s trailing control on a second, so the trailing controls of every row share an axis whatever the depth. The chip wording follows bazza/ui’s data-table-filter 2025.04.12. The named buckets around today come from Dice UI’s data table, its shadcn registry item, for the isRelativeToToday date operator, mapped onto the calendar operators this API documents. None of the three is installed.

One entity type’s fields, read through the context and kept. Every row of the editor asks it what a path is, so the chooser, the operator list and the value control all read one answer. The schema service caches, so this reaches the network once per type however often the editor redraws (probe 002).