Skip to content

[finding] The console filter builder mints a SCALAR value for the in / notIn operators, which the spec now refuses (objectstack#6227) #3958

Description

@os-zhuang

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:

  1. addCondition() (:226) seeds every new row as { operator: "equals", value: "" }.
  2. Changing the operator dropdown calls updateCondition(condition.id, { operator: value }) (:493) — it does not reset value, so the scalar "" survives the switch to in.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions