Repository navigation
feat(console): a stored view filter's OR group filters the list, and the filter builder saves and reads it - #28475
Conversation
… 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
|
domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4 Contract reviewServed-tier: ① Derived judgmentsThe face is the ② Semver levelpatch 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 flagsThe 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. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4 ACCEPT · 2026-10-31T22:20Z 席位核对
Generated by Claude Code |
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: [规则...] }现在会真正筛出数据(列表、导出、看板、日历、相关列表等都生效);筛选器里切到「或」可以保存成视图,再打开时仍是「或」。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 ownisViewFilterOrGroup; no reader restates the test.Query lowering.
@object-ui/coretoFilterNode: a group becomes anornode of its rules (one member: that rule's node), one child of the list's implicit AND. Members go throughviewFilterRuleToNode, so the operator fold, the arity check and theicontainscomparand refusal apply inside a group as at the top level. A shape the spec refuses (emptyor, non-arrayor, a blank-field member, a group inside a group) is returned as it arrived, an object in AST position that the server refuses with400 INVALID_FILTER, never repaired or dropped. This one sink coversbuildEffectiveFilter(list fetch and export),ObjectView(kanban, calendar, gallery, timeline),toFilterNodeSafely(related list, line items, grid) andValueDataSource.@object-ui/data-objectstacktranslator (bothfind()routes): a list opening with a group is recognised as the entry form; a group entry becomes['or', ...tuples]; a group spread into anandis translated when every member is a usable rule. A group that does not translate at the top level is refused whole withMalformedFilterError(INVALID_FILTER, 400). It used to ship a list opening with a group raw, and refuse a later group as an entry with nofield.Save.
foldFilterGroupToSpecRules:logic: 'or'over two or more rules folds to[{ or: rules }]instead of refusing withor_logic. Fewer than two rules still fold flat; a nested group is still refused (nested_group). Theor_logicreason and its messageconsole.objectView.filterOrNotSavableare retired in all ten packs.Read back.
@object-ui/coreexportbuilderRulesOfSpecFilter(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,nullfor anything else carrying a group.parseSpecFilter/toFilterGroup(@object-ui/plugin-view) read a lone group as an OR group of its rules.filter-builderfield (metadata-admin/widgets.tsx, also used for view tabs and the page block inspector) seeds the builder frombuilderRulesOfSpecFilter, 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 withconsole.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 nofield, 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 malformedoris kept for the server to refuse.fieldNamesHeldBycounts a group's member fields as fields the view references.PageBlockInspector's filter field passes the element type through.Docs.
content/docs/ui/views.mdxgains a Base filter section with the any-of group and the builder's behaviour.Branch: the harness-assigned
claude/project-thread-wthww8, notclaude/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 throughisFilterAST/parseFilterAST; one-member group; a refused member throwsFilterOperatorErrorandtoFilterNodeSafelyanswersok: false; four refused shapes stay refused byisFilterAST;mergeFilterNodeswith a user filter;ValueDataSourcerow sets for a group and a group ANDed with a rule;builderRulesOfSpecFiltercases.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 withINVALID_FILTER/ 400 on both routes.ui/packages/app-shell/src/views/viewFilterOrGroup.roundTrip-28370.test.tsx(new, 10): fold →ListViewSchemaaccepts →toFilterGroupreads 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;sanitizeViewOverridekeeps, trims and drops groups.viewFilterFold.test.ts, the twoor_logicrefusal cases now pin the fold to one group, and the reason list isnested_grouponly; two operator reads cast the folded element to a rule (type only).Acceptance notes
ui/packages/types/src/record-components.tsrider (the related-listfilterandadd.picker.filtermirror) is NOT in this PR, by the seat's ruling: widening it type-breaksui/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 throughtoFilterNodeSafely. 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/coreexportbuilderRulesOfSpecFilterand its typeBuilderFilterRules, 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.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.Verification
Head
4699b9fb, base776c1d87(an ancestor of currentmain). Headcfd88e75adds one commit on top: the changeset's Skills-docs line names #28476 and a type alias moves above a docblock inPageBlockInspector.tsx(no behaviour change; changeset gates and that file's eslint re-run, exit 0). All test runs underscripts/pm/os-verify-lock.shon a shared 4-core box.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 inviews/metadata-admin/or matchviews/*ilter*/views/*bjectView*, were run: 434 files, 3900 passed, 0 failed. CI runs the full suite.type-checkin all five touched packages, exit 0 (app-shell after the two test casts in4699b9fb).pnpm check:type-check-debt: 0 raw tsc errors, no ledger entry above its number.data-objectstack/src/index.tskeeps its existing inlineno-consoleallowances).git diff HEADempty after): core group loweringreturn group→ 7 of 13 red; adapter group translationreturn null→ 8 of 10 red; fold OR → flat rules → 4 of 10 red;sanitizeViewOverridegroup branch off → 2 of 10 red.dispatch-gates --commandsat4699b9fbderived 67; all 67 ran and exited 0 (check:skill-examplesafter building@objectstack/clientand@objectstack/client-react, which it reads);dispatch-gates --ran: 67 run, 0 NOT-MEASURED.ui/packages/types/**, including the related-list mirror pinsrelated-list-add-picker-filter-mirror-9964.test.tsandrelated-list-filter-rule-array-10199.test.ts.packages/specor protocol change; the console path is pinned in jsdom).Generated by Claude Code