Skip to content

feat(console): a stored view filter's OR group filters the list, and the filter builder saves and reads it - #28475

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-wthww8
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-wthww8

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#5 · os-sales · session_01L6bhgXa2W2ZCA4huS5hVVx

Requested by Sales · project thread

Fixes #28370

Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: follow-up #28476 — skills/objectstack-ui/rules/list-views.md, skills/objectstack-ui/evals/views-apps-actions-pages.json

维护者速读

  • 改了什么:已存视图筛选里的「或」分组 { or: [规则...] } 现在会真正筛出数据(列表、导出、看板、日历、相关列表等都生效);筛选器里切到「或」可以保存成视图,再打开时仍是「或」。
  • 为什么改:spec 已允许存这种分组,但控制台不认识它,带分组的视图加载时报筛选格式错误、一条记录都不显示;筛选器也拒绝保存「或」。
  • 风险与回滚:只改控制台对分组的读写;没有分组的筛选与之前逐字节相同。组与其他条件并列时,Studio 筛选器只读显示(防止保存丢条件),需在元数据里编辑。回滚即还原本 PR,无数据迁移。
  • 先看:ui/packages/core/src/utils/filter-converter.ts(toFilterNode 把分组降为 or 节点)、ui/packages/app-shell/src/views/viewFilterFold.ts(「或」保存为一个分组)、ui/packages/app-shell/src/views/objectViewTabs.ts(视图覆盖清洗不再丢分组)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/core/src/utils/__tests__/view-filter-or-group-28370.test.ts,预期 13 个用例全绿(分组降为 or 节点,内存数据源按「任一」筛出 a、b、e)。
  • 需要你做的:无,席位落地。

What changes

A view filter list element { or: [rule, ...] } (ViewFilterOrGroupSchema, spec half #28380) is read by every console path that lowers, saves or reads back a stored view filter. Element kinds are told apart by the spec's own isViewFilterOrGroup; no reader restates the test.

Query lowering.

  • @object-ui/core toFilterNode: a group becomes an or node of its rules (one member: that rule's node), one child of the list's implicit AND. Members go through viewFilterRuleToNode, so the operator fold, the arity check and the icontains comparand refusal apply inside a group as at the top level. A shape the spec refuses (empty or, non-array or, a blank-field member, a group inside a group) is returned as it arrived, an object in AST position that the server refuses with 400 INVALID_FILTER, never repaired or dropped. This one sink covers buildEffectiveFilter (list fetch and export), ObjectView (kanban, calendar, gallery, timeline), toFilterNodeSafely (related list, line items, grid) and ValueDataSource.
  • @object-ui/data-objectstack translator (both find() routes): a list opening with a group is recognised as the entry form; a group entry becomes ['or', ...tuples]; a group spread into an and is translated when every member is a usable rule. A group that does not translate at the top level is refused whole with MalformedFilterError (INVALID_FILTER, 400). It used to ship a list opening with a group raw, and refuse a later group as an entry with no field.

Save. foldFilterGroupToSpecRules: logic: 'or' over two or more rules folds to [{ or: rules }] instead of refusing with or_logic. Fewer than two rules still fold flat; a nested group is still refused (nested_group). The or_logic reason and its message console.objectView.filterOrNotSavable are retired in all ten packs.

Read back.

  • New @object-ui/core export builderRulesOfSpecFilter(stored): a stored list as the one-level builder's logic over flat rules: AND for plain rules, OR for a list that is exactly one group, null for anything else carrying a group.
  • parseSpecFilter / toFilterGroup (@object-ui/plugin-view) read a lone group as an OR group of its rules.
  • The Studio filter-builder field (metadata-admin/widgets.tsx, also used for view tabs and the page block inspector) seeds the builder from builderRulesOfSpecFilter, so a stored group opens with OR and an edit writes the group back. A list with a group beside other conditions is shown with its fields named and the trigger disabled, titled with console.objectView.filterOrGroupNotEditable (new, all ten packs): loading it into a one-level builder would drop part of it on the next save.

Other readers of a stored filter list.

  • sanitizeViewOverride (objectViewTabs.ts) dropped every group as an entry with no field, so a stored override with an OR group lost it on read and the view widened. A group is now judged by its members: unfinished members leave it, a group with none left is dropped, a malformed or is kept for the server to refuse.
  • The Studio list-view inspector's fieldNamesHeldBy counts a group's member fields as fields the view references.
  • PageBlockInspector's filter field passes the element type through.

Docs. content/docs/ui/views.mdx gains a Base filter section with the any-of group and the builder's behaviour.

Branch: the harness-assigned claude/project-thread-wthww8, not claude/issue-28370-*; the card is #28370.

Tests

  • ui/packages/core/src/utils/__tests__/view-filter-or-group-28370.test.ts (new, 13): fixtures parse as spec elements; a lone group, a group beside a rule (nested in the AND) and the HotCRM "open, or completed in the last 7 days" shape lowered and put through isFilterAST / parseFilterAST; one-member group; a refused member throws FilterOperatorError and toFilterNodeSafely answers ok: false; four refused shapes stay refused by isFilterAST; mergeFilterNodes with a user filter; ValueDataSource row sets for a group and a group ANDed with a rule; builderRulesOfSpecFilter cases.
  • ui/packages/data-objectstack/src/filter-or-group-28370.test.ts (new, 10): group alone, first, last and spread, on both routes, with the wire parsed by the spec; three untranslatable groups refused with INVALID_FILTER / 400 on both routes.
  • ui/packages/app-shell/src/views/viewFilterOrGroup.roundTrip-28370.test.tsx (new, 10): fold → ListViewSchema accepts → toFilterGroup reads OR → folds to the same body; AND control; the Studio widget opens a stored group as OR and writes edits back as the group, toggles AND ↔ OR between flat rules and one group, and shows a mixed list read-only with every field named and nothing written; sanitizeViewOverride keeps, trims and drops groups.
  • Moved pins: viewFilterFold.test.ts, the two or_logic refusal cases now pin the fold to one group, and the reason list is nested_group only; two operator reads cast the folded element to a rule (type only).

Acceptance notes

  • The claim's ui/packages/types/src/record-components.ts rider (the related-list filter and add.picker.filter mirror) is NOT in this PR, by the seat's ruling: widening it type-breaks ui/packages/plugin-detail/src/RelatedList.tsx, whose two prop types (filter, picker.filter) are held by another card thread. Runtime is unaffected: the related list lowers through toFilterNodeSafely. Follow-up: @object-ui/types: the related-list filter and add-picker filter accept a view filter's { or } group, as the spec carriers do #28462.
  • Clause-②: yes (widening) is now for the new @object-ui/core export builderRulesOfSpecFilter and its type BuilderFilterRules, not for the deferred rider.
  • Metadata-protocol: none: what a metadata author can write is unchanged (the spec half admitted the group); this PR makes the console apply it.
  • Spec follow-up (packages/spec, not touched here): ViewFilterOrGroupSchema's describe and TSDoc still say [PLANNED — not yet rendered] and that the list is refused when it loads. Once this lands, a one-line spec edit drops the marker and that sentence.
  • Not handled: a mixed list (a group beside other conditions) cannot be edited in the Console builder; it is applied, shown read-only, and editable in metadata. The builder has no sub-group control.

Verification

Head 4699b9fb, base 776c1d87 (an ancestor of current main). Head cfd88e75 adds one commit on top: the changeset's Skills-docs line names #28476 and a type alias moves above a docblock in PageBlockInspector.tsx (no behaviour change; changeset gates and that file's eslint re-run, exit 0). All test runs under scripts/pm/os-verify-lock.sh on a shared 4-core box.

  • New tests: core 13/13, data-objectstack 10/10, app-shell round trip 10/10.
  • Package suites: core, data-objectstack, plugin-view, i18n: 508 files, 7634 passed, 40 skipped, 0 failed.
  • app-shell: the full 2012-file suite did not finish here (the container restarted mid-run), so the 434 test files that import a changed module, or sit in views/metadata-admin/ or match views/*ilter* / views/*bjectView*, were run: 434 files, 3900 passed, 0 failed. CI runs the full suite.
  • Type-check: type-check in all five touched packages, exit 0 (app-shell after the two test casts in 4699b9fb). pnpm check:type-check-debt: 0 raw tsc errors, no ledger entry above its number.
  • ESLint on every changed file: 0 errors (warnings only; data-objectstack/src/index.ts keeps its existing inline no-console allowances).
  • Ablations (each restored, git diff HEAD empty after): core group lowering return group → 7 of 13 red; adapter group translation return null → 8 of 10 red; fold OR → flat rules → 4 of 10 red; sanitizeViewOverride group branch off → 2 of 10 red.
  • Gates: dispatch-gates --commands at 4699b9fb derived 67; all 67 ran and exited 0 (check:skill-examples after building @objectstack/client and @objectstack/client-react, which it reads); dispatch-gates --ran: 67 run, 0 NOT-MEASURED.
  • Does not touch ui/packages/types/**, including the related-list mirror pins related-list-add-picker-filter-mirror-9964.test.ts and related-list-filter-rule-array-10199.test.ts.
  • Dogfood: not owed (no packages/spec or protocol change; the console path is pinned in jsdom).

Generated by Claude Code

… and the filter builder

A view filter list element { or: [rule, ...] } now lowers to an or node of
its rules inside the list's AND (core toFilterNode and the data-objectstack
translator), the filter builder saves an OR group as one such element instead
of refusing it, and the Studio filter field and parseSpecFilter read a lone
group back as an OR group. A group beside other elements is shown read-only
in the one-level builder, and the view-override recovery pass keeps a group
instead of dropping it as a field-less entry.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXa2W2ZCA4huS5hVVx
…und trip and override recovery; add changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXa2W2ZCA4huS5hVVx
…ls-docs line in the changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXa2W2ZCA4huS5hVVx
…est reads the operator

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXa2W2ZCA4huS5hVVx
…field's docblock on its function

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXa2W2ZCA4huS5hVVx
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4

Contract review

Served-tier: TIER_DEFAULT
Head-sha: cfd88e75ea429dcb5ee1c59a9e47e7d8844dd578
Local-runs: none
Scope: ui/packages/core/src/utils/filter-converter.ts, ui/packages/data-objectstack/src/index.ts, ui/packages/app-shell/src/views/viewFilterFold.ts, ui/packages/app-shell/src/views/objectViewTabs.ts, ui/packages/app-shell/src/views/metadata-admin/widgets.tsx, ui/packages/plugin-view/src/config/view-config-utils.ts, ui/packages/i18n/src/locales/en.ts

① Derived judgments

The face is the Clause-②: yes (widening) declaration alone: no packages/spec/src/** file and no governed path in the diff. What widens: @object-ui/core exports builderRulesOfSpecFilter and its result type BuilderFilterRules; foldFilterGroupToSpecRules now returns ViewFilterElement[] (a rule or an { or } group) where it returned flat rules, and its or_logic refusal reason retires (the reason set narrows to nested_group, an app-shell internal, not on a package entry). The i18n key console.objectView.filterOrNotSavable retires and console.objectView.filterOrGroupNotEditable is added, in all ten packs. The stored view-filter shape is the spec's own (ViewFilterOrGroupSchema, spec half #28380): no authored key, accept set or error code moves; a group the spec refuses stays refused by the server (400 INVALID_FILTER), and the adapter refuses an untranslatable group whole with MalformedFilterError, never dropping it.

② Semver level

patch for every package, as the changeset declares: an additive export, a fold that writes what it used to refuse, and a reader that applies what it used to reject; a stored filter without a group lowers byte-for-byte as before.

③ Boundary flags

The Studio filter field shows a list with a group beside other conditions read-only (trigger disabled, reason in its title), so a one-level builder never saves part of it. sanitizeViewOverride keeps a group, trims its unfinished members and drops it only when none remain; it used to drop every group, which widened the view. The related-list filter type mirror in @object-ui/types is out of this PR (#28462, seat 4, lands after this one); ui/packages/types/** is untouched.

Implemented-by: session_01L6bhgXa2W2ZCA4huS5hVVx
Reviewed-by: session_01L6bhgUhKx1XHzkoikGjRB4

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4

ACCEPT · 2026-10-31T22:20Z
Head-sha: cfd88e7

席位核对

  1. Shape: 24 files (+766 / −82), Fixes #28370: toFilterNode (@object-ui/core) lowers a stored { or: [rule, ...] } element to an or node of its rules inside the list's AND, so the list, export, kanban, calendar, gallery, timeline, related lists, line items, grid and ValueDataSource all apply it; the data-objectstack translator handles a group on both find() routes and refuses an untranslatable one whole (INVALID_FILTER); the fold saves the builder's OR as one group; builderRulesOfSpecFilter reads a lone group back as OR in the Studio field and parseSpecFilter / toFilterGroup; sanitizeViewOverride stops dropping groups; views.mdx gains a Base filter section. Inside claim 6546577033 as amended by 6546690468 (the record-components.ts rider deferred to @object-ui/types: the related-list filter and add-picker filter accept a view filter's { or } group, as the spec carriers do #28462). packages/spec and ui/packages/types/** untouched. Not governed, 848 lines ≤ 3000 (check-governed-merges --pr 28475).
  2. Contract: Clause-②: yes (widening) (new @object-ui/core export builderRulesOfSpecFilter + type BuilderFilterRules; one i18n key retired and one added in ten packs); TIER_DEFAULT review record by this seat, PASS on this head (6549760015). Metadata-protocol: none holds: the spec half (feat(spec): a stored view filter list can hold an any-of group #28380) admitted the group; this PR makes the console apply it.
  3. Code read: each group member lowers through the same viewFilterRuleToNode as a top-level rule, so the operator fold and refusals hold inside a group; a malformed group is never repaired or dropped (server refusal stays what the author sees); a mixed list (a group beside other conditions) is applied but shown read-only in the one-level builder, so no save loses part of it; the retired filterOrNotSavable key has no remaining reader.
  4. Gates (check-runs on this head): 63 runs, 53 success, 10 skipped by path, 0 other; Test UI 16/16, Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Check Changeset success. The dev's battery: 33 new tests (core 13, data-objectstack 10, app-shell round trip 10), four ablations each red then restored, dispatch-gates --commands 67/67, 0 NOT MEASURED; the app-shell full suite is CI's reading (the container restarted mid-run locally).
  5. Changeset, read sentence by sentence: patch for the five touched packages; the Query / Adapter / Save / Read back / Override recovery paragraphs are each true against the diff; the Skills-docs line names skills: teach the view filter any-of group { or: [...] } in the objectstack-ui list-views rule #28476.
  6. Follow-ups: skills: teach the view filter any-of group { or: [...] } in the objectstack-ui list-views rule #28476 (skills list-views.md + eval teach the any-of group, domain:skills, Tier H); @object-ui/types: the related-list filter and add-picker filter accept a view filter's { or } group, as the spec carriers do #28462 (types related-list filter mirror, seat 4 PR feat(types): a related list's filter and add-picker filter take a view filter's any-of group #28466, lands after this one); spec seat: ViewFilterOrGroupSchema's [PLANNED — not yet rendered] describe marker can go once this is on main (pointer posted on Console list query and filter builder do not read a stored view filter's any-of group #28370 at landing).
  7. Landing: pr_ready + automerge_enable through the relay (Sales: seats land what they ACCEPT under pm-dispatch), MERGED confirmed; Console list query and filter builder do not read a stored view filter's any-of group #28370 then closes by Fixes.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 22:21
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 22:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 768ec4b Oct 31, 2026
64 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-wthww8 branch October 31, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console list query and filter builder do not read a stored view filter's any-of group

1 participant