Skip to content

fix(drill): the list URL carries a boolean, a set, $ne and the text operators, so Open in list and View all regain an exact destination - #30777

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-7ywrg2
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-7ywrg2

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #30743

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:对象列表页 /apps/APP/OBJECT/data 的 URL 新增六种写法:布尔 filter[F][bool]=true|false、$ne 与五个文本运算符 filter[F][ne|contains|notcontains|startswith|endswith|icontains]=V、集合 filter[F][in]=A&filter[F][in]=B(及 [nin])。下钻「在列表中打开」、drillDown.target: 'navigate' 和相关列表「查看全部」遇到这些条件时恢复去处。
  • 为什么改:Drill drawer "Open in list" opens a wider list than the drawer when the drill filter has two conditions on one field or an $in #30654 之后,凡是 URL 说不全的下钻都不给出口;按是/否字段分组的图表、status in (...) 这类度量范围最常见,全部没了去处。实测(见下):布尔原样写出会读回成字符串 'true',其余写法根本写不出参数。
  • 风险与回滚:只放宽「能精确写出」的集合,每个新写法读回后必须与抽屉查询的 AST 节点完全一致(含类型)才放行;$or、$between、空集合、布尔的 $ne 仍然不给出口。旧链接 filter[F]=true 含义不变(文本)。回滚即 revert。
  • 先看:ui/packages/app-shell/src/views/drillUrlFilters.ts(新常量与读写两侧)、ui/packages/app-shell/src/views/useOpenRecordList.ts(exactRecordListFilter 的接受集与 isSameComparison)、ui/packages/app-shell/src/views/ObjectDataPage.tsx(筛选标签渲染与「另存为视图」桥接)。
  • 两分钟验证:打开 /apps/APP/OBJECT/data?filter[status][in]=paid&filter[status][in]=sent,预期列表只含这两种状态,顶部标签显示「In paid, sent」(中文包为对应译文),点标签上的 ✕ 两个参数一起移除。
  • 需要你做的:无,席位落地。

What changes

Measured first, on origin/main b3098661, through the real toFilterNode → parseFilterAST → serializeDrillFilterParams → parseUrlFilterTriples chain, against convertFiltersToAST (what the drill drawer queries):

condition drawer node URL written before read back before
{ is_active: true } [is_active, '=', true] filter[is_active]=true [is_active, '=', 'true'] (a string)
$in / $nin [status, 'in', [...]] nothing nothing
$ne [status, '!=', 'void'] nothing nothing
$contains, $notContains, $startsWith, $endsWith, $icontains [name, 'contains', 'acme'] … nothing nothing

All 18 probed cases answered exactRecordListFilter → null. After this PR every one of them round-trips to the drawer's own node, except the three that must not (an empty $in, a boolean $ne), which still answer null.

ui/packages/app-shell/src/views/drillUrlFilters.ts

  • BOOLEAN_FILTER ([bool]): the only two spellings are true / false, read back as the boolean; anything else in that slot is dropped, never read as text. A bare boolean in a drill filter is written there instead of filter[F]=true.
  • URL_COMPARE_OPS / COMPARE_OP_PARAM: ne → '!=' and the five text operators, suffix → the AST word convertFiltersToAST emits. Prototype-free, like URL_FILTER_OPS, which keeps its four pinned entries.
  • URL_LIST_OPS / LIST_OP_PARAM: in / nin as a repeated param, one member per occurrence (no delimiter, so a member may contain a comma). The read side gathers every occurrence on one field into one triple at the first occurrence's position. The write side writes the whole list or nothing (an empty list, an empty member or a boolean member writes no param), and a later source replaces an earlier list on the same field, the set posture of every other param.
  • URL_TRIPLE_OP_WORD: AST word → canonical ViewFilterRule word, read by the chips (label key filterBuilder.operators.WORD, existing in all ten packs) and by "Save as view".
  • groupFilterChips: a negation, text or membership chip carries the operator label as textKey and the comparand as text (a set joined with a comma). FilterChip.textKey's doc now says it may sit beside text. Every existing chip is unchanged.

ui/packages/app-shell/src/views/useOpenRecordList.ts

  • exactRecordListFilter accepts a boolean under equality (bare or $eq), $ne with a string or finite number, the text operators with a non-empty string, and $in / $nin with a non-empty list of strings or finite numbers.
  • Each condition's proof is stronger: written alone it must read back as one triple (now: one param NAME, repeated for a set), AND that triple must equal convertFiltersToAST(condition), field, operator and comparand, with the one difference the route has always carried (a finite number reads back as its text). A boolean reading back as 'true' fails this check.

ui/packages/app-shell/src/views/ObjectDataPage.tsx

  • The chip renders t(textKey) + ' ' + text when it carries both.
  • TRIPLE_OP_TO_SPEC_ALIAS spreads URL_TRIPLE_OP_WORD, so "Save as view" folds != to not_equals, nin to not_in, and so on; the record gate passes.

One changeset: @object-ui/app-shell patch.

Tests

  • drillUrlFilters.test.ts: a new describe for the six spellings: write/read for each against convertFiltersToAST, only two [bool] spellings, filter[name]=true still text, set order and grouping, whole-list-or-nothing, later set replaces, boolean $ne writes nothing, prototype sweep over the new lookups, chips, prefix delete of a set, equality-only route still executes none.
  • exactRecordListFilter-25888.test.ts: the new accepted spellings round-trip; each equals the drawer node; numbers in a set; a set beside an equality; a repeated set written once; new refusals (empty set, empty member, boolean member, two different sets, boolean $ne, text operator with a number, $between).
  • useOpenRecordList.placeholderScope-9022.test.tsx: through the real host, an $in scope and a boolean bucket open the exact URL; a boolean $ne still opens nothing.
  • ObjectDataPage.saveAsViewFilterFold.test.ts: every new operator folds to a canonical rule and the ViewItemSchema gate passes; the operator sets are computed from the exported maps.
  • ObjectDataPage.filterChipI18n-9159.test.tsx: on the real page with the real packs, [in] / [nin] / [ne] chips draw the zh label beside the comparand, and the en control differs.

Moved pins (intended, each the behaviour this card asks to change):

  • exactRecordListFilter-25888: four refusals (set operator, negation, text operator, boolean comparand) became accepted cases.
  • useOpenRecordList.placeholderScope-9022: "an $in scope offers no destination" now opens the exact list.
  • ObjectView.urlFilterSuffix-9196: row 1 used [contains] as its unknown suffix; it now uses [like], and a new row pins [contains] executing on /data only while the object route still drops it. Its non-vacuity battery gains the /data contains triple.

Acceptance notes

  • A boolean chip reads = true / = false: the raw comparand, the posture every equality chip already has (an option label is not drawn either).
  • A set of numbers reads back as the members' text (['1', '2']), the posture equality and the range bounds have always had; the exactness check allows exactly that difference and nothing else.
  • Still refused, by design: $or and every other combinator, $between, an empty set, a set member that is empty or boolean, $ne on a boolean, a text operator with a non-string, two different comparands or sets in one param.
  • The plain object route (ObjectView, equality-only, objectui#9196's boundary) learns none of the new suffixes.
  • ui/packages/i18n/src/locales/*.ts untouched: the chip labels are the filter builder's existing filterBuilder.operators.* keys.
  • Serial: PR feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships #30595 also adds lines to ObjectDataPage.filterChipI18n-9159.test.tsx; this PR appends one describe at the end of that file. Whichever lands second merges the other.
  • NOT MEASURED: a live browser run of the Console; the chip and navigation behaviour is covered through the real ObjectDataPage render and the real useRecordListDrillNavigation host in the tests above.

Verification

All readings on head ed322918.

Targeted suites: 21 files, 383 tests, all green

pnpm exec vitest run --maxWorkers=2 from ui/ over drillUrlFilters.test.ts, exactRecordListFilter-25888, both RelatedRecordActionsBridge View-all suites (12741, 25888), useOpenRecordList.placeholderScope-9022, ObjectView.urlFilterSuffix-9196, ObjectView.filterParamsKeyEncoding-9287, every ObjectDataPage.* suite, every drill* suite in app-shell views, studio-canvas-preview, and plugin-dashboard's DatasetWidget and ObjectPivotTable.drillFilterComposition-9024: Test Files 21 passed (21), Tests 383 passed (383). Before the pin moves the same run read 7 failed | 339 passed, the seven moved pins listed above and nothing else.

Type-check: clean

pnpm --filter @object-ui/app-shell type-check (both tsc --noEmit and tsc -p tsconfig.test.json, after building the @object-ui/app-shell^... closure): 0 error TS, VERDICT command-exit 0.

Ablation: one mutation, three reds, restored

node scripts/ablation-replace.mjs on drillUrlFilters.ts, anchor out.push([field, '=', true]) in the [bool] read arm replaced with the string 'true' (anchor 1 → 0, blob 35873c7f → 0120ef72): drillUrlFilters.test.ts "reads the [bool] slot back as the BOOLEAN the drawer compares with" red, and exactRecordListFilter-25888 "writes the spellings objectstack#30743 added" plus "reads each of those back as the node the drill drawer queries" red (the gate's comparand-type check refusing the string). Tests 3 failed | 116 passed. Restored: blob == HEAD, git diff HEAD empty.

Derived gates

node scripts/pm/dispatch-gates.mjs --commands names 35 families for this change set (10 paths, 642 changed lines, under the 3000 threshold). Running locally at open time; the readings and the --ran reconciliation are in the os-dev-report comment on #30743. Every family CI runs is read from this PR's check-runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's run.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief is the repo's Chinese 速读 block).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgW9yrPaVhdtD84RWcR

…perators, so "Open in list" and "View all" open the exact list again

The object list route gains `filter[FIELD][bool]=true|false`, the
`[ne|contains|notcontains|startswith|endswith|icontains]` suffixes and the
repeated `[in]` / `[nin]` membership params. Each reads back through
parseUrlFilterTriples as the node convertFiltersToAST lowers the same
condition to, comparand type included, and exactRecordListFilter now proves
that per condition before it accepts one. A drill on a yes/no bucket or under
an $in / $ne / text-operator scope, and a related list's "View all" with such
a filter, get their list destination back; $or, $between, an empty set and a
boolean under $ne stay refused.

Claude-Session: https://claude.ai/code/session_01L6bhgW9yrPaVhdtD84RWcR
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

Contract review

Served-tier: TIER_DEFAULT
Head-sha: ed3229182f4c7c56ce16e5fee007ef32c98201c8
Local-runs: none

Face: declaration only (Clause-②: yes (widening) in the PR body and .changeset/30743-list-url-boolean-in-ne-text.md). check-governed-merges --pr 30777: 0 of 10 paths governed; no packages/spec/src/**, no narrowing line. Read: the full diff at this head, the changeset, the os-dev-report 6675412242, and the cited code on main (convertFiltersToAST's operator words, ViewFilterOperator in packages/spec/src/ui/view.zod.ts, the filterBuilder.operators block in the ten locale packs).

① Derived judgments

  1. Read side, parseUrlFilterTriples. Three accepted spellings are new: filter[F][bool]=true|false → [F, '=', true|false] (any other value dropped, as in the [null] / [empty] slots); filter[F][ne|contains|notcontains|startswith|endswith|icontains]=V → [F, '!=' | op, V]; repeated filter[F][in|nin]=V → one [F, 'in' | 'nin', [V…]] at the first occurrence's place, members in URL order. Correct. The equality arm is untouched, so filter[F]=true still means the text 'true': no existing spelling changes meaning. The only change of meaning is for a URL that already carried one of the eight suffixes: it was an ignored unknown suffix and now filters. No producer on main writes any of them (git grep of ][ne] … ][bool] over ui/packages, apps, packages, tests excluded). The one pin that used [contains] as its "unknown suffix" (ObjectView.urlFilterSuffix-9196 row 1) moved to [like]. That is the declared widening, and the right one.
  2. The maps are prototype-free (Object.create(null)) for URL_COMPARE_OPS, URL_LIST_OPS and URL_TRIPLE_OP_WORD, the posture URL_FILTER_OPS already gives a suffix taken from the address bar. URL_FILTER_OPS keeps its four pinned keys. Correct.
  3. Write side, collectFilterParams. A boolean writes [bool]. $ne and the text operators write only a comparand that reads back as itself (non-empty string, finite number), so a boolean $ne writes nothing. $in / $nin write the whole list or nothing; an empty list or a member the route cannot carry skips the condition, because a partial set would change the answer. A later source replaces an earlier list on the same param, the set posture of every other param here. A bare array still writes nothing. Correct: the drill degrades to "no destination", never to a wider list.
  4. exactRecordListFilter. It now admits a boolean equality, $ne, the five text operators with a text operand, and a non-empty $in / $nin. Every admitted condition must pass two checks: one param name whose round trip yields exactly one triple, and isSameComparison against convertFiltersToAST(condition). That second check compares operator and comparand type, allowing only a finite number reading back as its own text (the posture equality and the bounds already had). Two sources agreeing on a param are written once, and two disagreeing refuse. $or, $between, an empty set and a boolean $ne stay refused. Correct, and stricter than before for every spelling: the drawer-node equality now also guards the old spellings.
  5. Chips. A negation, text or membership triple draws filterBuilder.operators.<word> before the comparand (set members comma-joined). Each word (not_equals, contains, not_contains, starts_with, ends_with, icontains, in, not_in) is an existing key, present in all ten packs (read in the zh block). A field with a range bound keeps its range chip. One chip per field is the existing posture. Correct; no new i18n key.
  6. Save as view. TRIPLE_OP_TO_SPEC_ALIAS spreads URL_TRIPLE_OP_WORD, whose words are canonical ViewFilterOperator members (view.zod.ts). The new fold pin computes its rows from both maps and asserts the record gate passes. Correct.
  7. Exports. Six new named exports in drillUrlFilters.ts (BOOLEAN_FILTER, URL_COMPARE_OPS, COMPARE_OP_PARAM, URL_LIST_OPS, LIST_OP_PARAM, URL_TRIPLE_OP_WORD). The module is not re-exported from @object-ui/app-shell's entry, so the package's published surface gains nothing. The one-authority gate ran in CI and is green.
  8. Observation, not a must-fix: ui/packages/plugin-detail/src/RelatedList.tsx (about lines 1705–1716 on main) says URL_FILTER_OPS is "the repo's one operator contract for this spelling" and that filter[<field>][contains]= "would arrive as no condition". After this PR the console route reads [contains] and [in]. The comment's conclusion still holds: that raw-URL fallback fetches an arbitrary api, which never reads this grammar, and the platform REST surface takes a JSON filter instead. Only its cross-reference is now stale. Recorded for the next change to that file, not this PR.

② Semver level

@object-ui/app-shell: patch, Metadata-protocol: none. AGENTS.md (changeset rule): a non-protocol change, widening or breaking, ships patch. The URL route and the drill destination are not something a metadata author writes, and "Save as view" folds to operators the spec already has, so none is right. Consistent.

③ Boundary flags

The os-dev-report 6675412242 has open_questions: [] and security_findings: none. Its two out-of-scope notes are answered:

Implemented-by: claude/project-thread-7ywrg2
Reviewed-by: session_01L6bhgWn6zhNa4LL7VB1rtL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

维护者速读

  • 改了什么:仪表盘下钻的"在列表中打开"和相关列表的"查看全部",原来遇到是/否字段、多选集合($in / $nin)、不等于($ne)和文本运算符(包含、开头是、结尾是等)时没有列表入口。现在列表页地址多了几种写法:filter[字段][bool]=true|false、filter[字段][ne|contains|…]=值、重复的 filter[字段][in]=值。读回来的条件和下钻抽屉查询的完全一样,连值的类型都一致。
  • 为什么改:Drill "Open in list" and related-list "View all" are withheld when the drill holds a boolean, $ne, $in or text-operator condition: the list route's URL cannot carry them #30743。实测 main 上 18 种情况都拿不到列表入口;是/否字段写成 filter[字段]=true 会被读回成文字 "true",和抽屉查的不是同一个条件。
  • 风险与回滚:只改 @object-ui/app-shell 前端,不改服务端、spec、文案键。地址里原来能写的写法含义都不变,filter[字段]=true 仍然表示文字。表达不了的条件($or、$between、空集合、布尔不等于)仍然不给入口,不会打开一个更宽的列表。回滚即撤本 PR。
  • 先看:ui/packages/app-shell/src/views/drillUrlFilters.ts 新增的三组写法,以及 useOpenRecordList.ts 里的 isSameComparison:每个条件都要和抽屉查询的条件逐项相同才放行。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/app-shell/src/views/drillUrlFilters.test.ts packages/app-shell/src/views/__tests__/exactRecordListFilter-25888.test.ts,预期全绿。
  • 需要你做的:无,席位落地。
  • 席位意见:可合。列表页的筛选条用筛选器现成的运算符翻译显示,比如"不等于 paid";"另存为视图"也会保留这些条件。

席位核对 — ACCEPT · head ed32291

  • 形态:10 个文件,+609/−33。check-governed-merges --pr 30777:0/10 条路径落在受管面,642 行 ≤ 3000。Fixes #30743,目标 main。只动 ui/packages/app-shell/src/views/ 下的源码和测试,加一个 changeset,都在认领 6674571486 和修订 6674648580 的文件面内。
  • 契约:Clause-②: yes (widening),Metadata-protocol: none,@object-ui/app-shell patch。PR 正文和 changeset 两处一致。申报面的 TIER_DEFAULT 复核记录 6675437187 判 PASS,head 相同。①逐条判了读、写两侧的新写法,exactRecordListFilter、筛选条、另存为视图和导出;②定级与 AGENTS.md 的 changeset 规则一致;③答复了 dev 的两条范围外说明。
  • 席位自己核对:读了整份 diff。八个新后缀在 main 上没有任何生产者写过(排除测试后 git grep)。三张新表都没有原型链,URL_FILTER_OPS 仍是固定的四个键。集合要么整组写出,要么不写。布尔不等于和空集合仍然不给入口。筛选条用的 8 个运算符键在十个语言包里都有。另存为视图用的词都是 ViewFilterOperator 的规范词。新常量没有从包入口导出,所以没有新增公开面。
  • 前提实测:dev 报告 6675412242(status done)。在 b3098661 上 18 个探测用例全部拿不到入口;布尔值读回成字符串 'true'。实现后定向跑 21 个文件 383 例全绿;改钉前红 7 例,正好是移动的 7 个旧钉。消融:把 [bool] 读回改成推字符串,红 3 例,恢复后 blob 与 HEAD 相同。type-check 0 错。派生门禁 35/35 exit 0,--ran 0 NOT-MEASURED。
  • 门禁 check-runs:head 上 55 个,44 success,11 skipped,0 红(Test UI 16 片、Type Check UI、Lint UI、Build & E2E UI、Live E2E UI 都绿)。mergeable_state clean。
  • 接受的偏差:本地只跑了相关的 21 个文件,app-shell 全包交给 CI 的 Test UI,已绿。是/否条件的筛选条显示 = true / = false;数字集合读回成文字。这两点和现有等值、范围条件的处理方式一致,PR 验收说明里已写明。
  • 范围外:ui/packages/plugin-detail/src/RelatedList.tsx(main 上约 1705–1716 行)的注释说 URL_FILTER_OPS 是这种写法唯一的运算符约定,还说 [contains] 会被丢掉。本 PR 之后,这句交叉引用过时了,但结论仍然成立:那个原始 URL 兜底请求的是任意 api,从不读这套写法。留给下次改该文件的卡,不另立卡。本 PR 和 PR feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships #30595 都在 ObjectDataPage.filterChipI18n-9159.test.tsx 末尾加内容,后合并的一方负责合并冲突。
  • 落地条款:中继 pr_ready + automerge_enable。合并后确认 Drill "Open in list" and related-list "View all" are withheld when the drill holds a boolean, $ne, $in or text-operator condition: the list route's URL cannot carry them #30743 关闭、摘 pm:dispatched、写落地评论,通知卡线程收尾。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 02:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 6, 2026 02:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit f8195bd Nov 6, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-7ywrg2 branch November 6, 2026 03:27
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant