Blocked-by: objectstack-ai/objectstack#6227
Out-of-scope observation from implementing objectstack#6227 (which constrains ViewFilterRuleSchema.value by operator). Unassigned, not queued. Duplicate search: no open issue in this repo matches "filter builder operator value array".
Fact
FilterBuilder (packages/components/src/custom/filter-builder.tsx) can emit a filter row whose operator is in / notIn while its value is a scalar, by three steps that are each individually reasonable:
addCondition() (:226) seeds every new row as { operator: "equals", value: "" }.
- Changing the operator dropdown calls
updateCondition(condition.id, { operator: value }) (:493) — it does not reset value, so the scalar "" survives the switch to in.
renderValueInput() (:306) only renders a multi-select widget when field?.options && isMultiOperator (:324) or for lookup-like fields (:312). A plain text/number field with no static options therefore falls through to the single-value <Input> regardless of operator, so whatever the user types stays a scalar.
foldFilterGroupToSpecRules (packages/app-shell/src/views/viewFilterFold.ts:106-126) then carries value through verbatim — it normalizes the operator but never the value shape:
if (c.value !== undefined) rule.value = c.value as ViewFilterRule['value'];
Why it matters now
This shape was already broken end-to-end before objectstack#6227 — it saved cleanly and then answered 400 INVALID_FILTER when the view was rendered (objectstack#5869). What changes is when the user finds out: after objectstack#6227 the spec schema refuses it at save time, so the console will surface a validation refusal on a row its own UI led the user into building.
That is a strict improvement in timing and a regression in felt UX unless the builder stops minting the shape: the user picks a legitimate operator from the dropdown the builder offered, types a value into the input the builder rendered, and is refused.
Secondary observation — one spelling adrift
isMultiOperator is decided from a local literal (:308):
const isMultiOperator = ["in", "notIn"].includes(condition.operator)
notIn is an alias in the spec vocabulary, not the canonical member — the canonical spelling is not_in, and normalizeFilterOperator folds one to the other. Any code path that hands this component a canonical not_in (for example a stored view read back) gets isMultiOperator === false and renders the single-value input for a set operator. objectstack#6227 exports VIEW_FILTER_LIST_VALUE_OPERATORS and VIEW_FILTER_PAIR_VALUE_OPERATORS from @objectstack/spec precisely so this list stops being a second local dialect.
Suggested direction (not pre-judged)
- Reset / re-shape
value when the operator changes between scalar-valued and list-valued families.
- Render a multi-value input for
in / not_in even when the field has no static options (a token/tag input), and a two-bound input for between.
- Decide the operator families from the spec's exported vocabularies rather than a local literal.
Refs: objectstack#6227, objectstack#5869.
Generated by Claude Code
Generated by Claude Code
Blocked-by: objectstack-ai/objectstack#6227
Out-of-scope observation from implementing objectstack#6227 (which constrains
ViewFilterRuleSchema.valueby operator). Unassigned, not queued. Duplicate search: no open issue in this repo matches "filter builder operator value array".Fact
FilterBuilder(packages/components/src/custom/filter-builder.tsx) can emit a filter row whose operator isin/notInwhile itsvalueis a scalar, by three steps that are each individually reasonable:addCondition()(:226) seeds every new row as{ operator: "equals", value: "" }.updateCondition(condition.id, { operator: value })(:493) — it does not resetvalue, so the scalar""survives the switch toin.renderValueInput()(:306) only renders a multi-select widget whenfield?.options && isMultiOperator(:324) or for lookup-like fields (:312). A plain text/number field with no staticoptionstherefore falls through to the single-value<Input>regardless of operator, so whatever the user types stays a scalar.foldFilterGroupToSpecRules(packages/app-shell/src/views/viewFilterFold.ts:106-126) then carriesvaluethrough verbatim — it normalizes the operator but never the value shape:Why it matters now
This shape was already broken end-to-end before objectstack#6227 — it saved cleanly and then answered
400 INVALID_FILTERwhen the view was rendered (objectstack#5869). What changes is when the user finds out: after objectstack#6227 the spec schema refuses it at save time, so the console will surface a validation refusal on a row its own UI led the user into building.That is a strict improvement in timing and a regression in felt UX unless the builder stops minting the shape: the user picks a legitimate operator from the dropdown the builder offered, types a value into the input the builder rendered, and is refused.
Secondary observation — one spelling adrift
isMultiOperatoris decided from a local literal (:308):notInis an alias in the spec vocabulary, not the canonical member — the canonical spelling isnot_in, andnormalizeFilterOperatorfolds one to the other. Any code path that hands this component a canonicalnot_in(for example a stored view read back) getsisMultiOperator === falseand renders the single-value input for a set operator. objectstack#6227 exportsVIEW_FILTER_LIST_VALUE_OPERATORSandVIEW_FILTER_PAIR_VALUE_OPERATORSfrom@objectstack/specprecisely so this list stops being a second local dialect.Suggested direction (not pre-judged)
valuewhen the operator changes between scalar-valued and list-valued families.in/not_ineven when the field has no staticoptions(a token/tag input), and a two-bound input forbetween.Refs: objectstack#6227, objectstack#5869.
Generated by Claude Code
Generated by Claude Code