Uh oh!
There was an error while loading. Please reload this page.
fix(components): FilterBuilder 值落在选项集外时清到空形或显示出来,不再隐形 (#4874) - #5030
Conversation
带 `options` 的 select/lookup 列,其值控件是受控的 Radix Select,而 `SelectValue` 只认已挂载的 `SelectItem`。于是文本列的 `equals "acme"` 指到 picklist 列(选项 `won` / `lost`)之后,值控件显示空,行里仍是 `value: "acme"` —— `foldFilterGroupToSpecRules` 照样持久化,实时网格照样拿 `stage equals "acme"` 去查。#4768(operator)、#4781(值的类型)之后「看不见的值」的第三张脸,成因是 值域而不是类型:`select` / `lookup` 属于文本族,`"acme"` 在类型上装得下。 按 2026-08-17 维护者裁定(A + C 组合): - `narrowFilterValueToOptions` 是 A 半边:切 field 时对**静态选项列**做成员判定, 不匹配的值清到 #4781 那套空形(标量 `''`、列表逐项过滤后 `[]`)。逐项而非连坐 —— 每一项都是独立的筛选项,一颗坏项不该丢掉用户写对的那几项; - `hasStaticOptionDomain` 是 A/C 分流的唯一机械判据:`options` 是**非空数组** ⇒ 静态列,选项集就是它的全部值域;`options` 缺席 / `options: []`(未到位)/ 非数组 ⇒ 远程异步列,本地选项集为该值背书不了,一律原样保留; - C 半边在渲染路径,且**无条件**成立:Select 把选项外的值挂成一个临时项(标签用值 本身,与 `LookupValuePicker` 对没有 label 的 id 的做法一致),多值列表把它渲染成 一行已勾选、可取消的条目。无论值是切列带来、从已存视图读回,还是选项集晚到才对 不上,控件都显示行里真正带着的东西 —— 也因此不需要为了显示去改写传入的 `value`。 - ⛔ B(保值但容忍隐形)维持否决:「行带值、控件空白」这个形态不再存在。 Co-authored-by: Claude <noreply@anthropic.com>
反向验证第三条(把远程列也接成员清空)撤掉 `hasStaticOptionDomain` 之后,这条用例 不是答错而是在 `optionKeysOf` 里抛异常(对字符串 `.map`)。判据函数同时是这条路径 的全性保证,把这个实测方向写在用例旁边,免得下一个人以为靠的是读取端的宽容。 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
外部传入的 `["acme","acme"]` 会画出两行同 key 的临时条目,而取消勾选是按值过滤的 (一次去掉全部同值项),所以一行才是「取消它会发生什么」的诚实计数。 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 17, 2026
CI 状态记录(head 这不是本 PR 的红,是当时 CI 侧的普遍降速,证据三条:
被卡住那 6 项对应的本地证据已全部跑过并在正文里: 按分派纪律本 PR 保持 draft,不自行 undraft、不入合并队列;请在 CI 恢复后复看这 6 项。 Generated by Claude Code |
yinlianghui
commented
Aug 17, 2026
更正上一条评论(5318508367):它的两条主要依据是错的,由我读了过期数据得出,在此撤回。 错在哪:上一条依据的是
因此「CI 侧普遍降速」这个结论不成立,请勿据此为本 PR 的任何红开脱。上一条唯一站得住的是第 3 点:首版 head 的 按 jobs 端点核出的真实状态(head 仍未完成:Test shard 1/4、2/4、4/4 —— 三者的 PR 保持 draft。 Generated by Claude Code |
yinlianghui
commented
Aug 17, 2026
最终更正:CI 全绿,上面两条评论关于「卡住/未完成」的说法全部作废。 按 jobs 端点逐个核对(head
连同 Lint(3m48s)、Type Check(6m41s)、Bundle Analysis、Build & E2E、Live E2E、Control Byte Scan、三条 Changeset 门禁、label 等:20 项 check、0 failure( 根因是我的观测方法,不是 CI,也不是本改动: 至此本 PR 的远端验证与本地验证结论一致(本地:components Generated by Claude Code |
yinlianghui
commented
Aug 17, 2026
PM 验收 ✅ ACCEPT(#4874,批次 21 —— 维护者 05:03Z A+C 裁定实施件) 实物核验(merge-base CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿;shard 4 于 18:19:10Z 收官,本 PM 同源复读与 dev 的 jobs 端点核验一致)。 判据采信: CI 状态双误报的处置:dev 前两条 CI 评论错误、第三条公开逐条更正作废 —— 根因是 #5031(空数组 lookup 列拒远程搜索 —— 改一整类列画哪个控件,属用户可见行为变更)按其自述转 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…remote picker The remote-picker branch in `renderValueInput` tested `!field?.options`, and `[]` is truthy, so a lookup column with `referenceTo` and an empty options array never reached `LookupValuePicker`: scalar operators drew a Select with zero candidates and no search box, `in`/`notIn` drew an empty checkbox list. The same column with the `options` key absent got full remote search. PR #5030 had already established the single criterion for this question — `hasStaticOptionDomain(field)` (options is a non-empty array) — and ruled `options: []` to be remote/not-yet-in-place on the value-domain side. The two sides therefore disagreed about one column. Per the maintainer ruling of 2026-08-17, the branch now reads that same criterion. Non-empty `options` keeps the static Select; the branch's other conjuncts are unchanged, so a column with nothing to search against routes exactly as before. Fixes#5031
Fixes#4874
实施依据 = 2026-08-17 05:03Z 维护者裁定(A + C 组合,B 维持否决),不是卡面的开放三选。
前提验证(jsdom,
origin/maine71c854)卡面症状原样复现,而且是三张脸而非一张:
acme→ 切到 picklist 列(won/lost)value: "acme"(照样持久化、照样下发查询)""options: []的 lookup 列带acc_007acc_007""(空 Select)in ["acme","won"]acmeWon/Lost两个勾选框,acme哪儿都没勾第二、三张脸此前没被单独记过:前者是「
optionspresent-but-empty」——[]是真值,所以它进不到远程 picker 分支,落进一个空 Select;后者是同一机制的多值面。A/C 分流的机械判据
hasStaticOptionDomain(field)=options是非空数组。这是全 PR 唯一的分流点:options缺席 /options: [](未到位)/ 非数组 ⇒ 远程异步列:本地选项集为该值背书不了,一律原样保留。options缺席正是renderValueInput自己那条远程 picker 分支的条件(!field?.options);options: []不是「值域为空」而是「还没到」——@object-ui/fields的deriveFilterFields把Array.isArray(f.options)原样映射过来,picklist 值没到位时产出的就是它,对它做成员判定会清掉世上任何值。第二个判据
isOptionDrivenValueControl(field, operator)照renderValueInput的分支顺序回答「该控件是否只能显示选项内的值」:list→ 勾选框列表(是);pair→between画两个普通区间框、从不读 options(否,结构性回答,不靠「select 桶今天不给 between」这个巧合);scalar→ 只对 select/lookup 族画 Select(text列即使带 options 也仍是普通输入框,什么都显示得出来,因此不判)。改动
narrowFilterValueToOptions:切 field 时对静态选项列做成员判定,不匹配清到 FilterBuilder 切换行的 field 后 value 原样留下 —— 文本值落到 number 列,输入框显示空而行仍带着该值 #4781 那套空形(标量''、列表逐项过滤后[]、pair不动)。列表逐项而非连坐——每项都是独立筛选项,一颗坏项不该丢掉用户写对的那几项(["won","acme","lost"]→["won"])。成员判定用String()两端比,与控件判定可见性的方式逐字一致(勾选框的selectedValues.map(String).includes(String(opt.value))、Radix 拿SelectValue匹配已挂载的SelectItem value)——两个答案一旦分叉,就又回到「行带值、控件显示不出来」。顺带钉住 FilterBuilder 值控件把「假值」当成「空」—— 布尔 false 与数字 0 显示为空,行却带着该值 #4873 的交界:选项 id'0'不会被当成「未填」清掉。SelectItem,标签用值本身——与LookupValuePicker对没有 label 的 id 的做法(resolved[id] || id)同一个答案;多值勾选框列表把它渲染成一行已勾选、可取消的条目。无条件是有意的:控件欠的不变量是「trigger 显示行里真正带着的东西」,而它带着什么与怎么带来的无关(切列、已存视图读回、选项集晚到)。也因此不必为了显示去改写传入的value——静默改写一份已存视图比隐形更糟。LookupValuePicker(options缺席的真远程列)本来就把 id 兜底显示出来,未改动,只是补了钉子。验证
packages/components/src/__tests__/filter-builder-value-option-domain.test.tsx(25 项):静态列清空 + 空形断言、远程列保值 + 临时项可见(断言 trigger 文本 = 值且非空串)、值在选项集内不清(blast-radius 对照)、narrowFilterValueToOptions判据矩阵。pnpm exec vitest run packages/components/src/__tests__/→ 89 files / 834 tests passed(含 FilterBuilder 切换行的 field 后 operator 不重置 —— select 改 text 后 operator 可停留在下拉里已不存在的 in,trigger 显示空 #4768 / FilterBuilder 切换行的 field 后 value 原样留下 —— 文本值落到 number 列,输入框显示空而行仍带着该值 #4781 / FilterBuilder 值控件把「假值」当成「空」—— 布尔 false 与数字 0 显示为空,行却带着该值 #4873 / [finding] The console filter builder mints a SCALAR value for thein/notInoperators, which the spec now refuses (objectstack#6227) #3958 既有钉全量)。packages/fields/(FilterConditionField)、packages/plugin-list/(ListView)、packages/plugin-view/、packages/data-objectstack/→ 196 files / 2995 tests passed;packages/app-shell/src/views/(viewFilterFold / ObjectDataPage / metadata-admin widgets)→ 261 files / 2464 passed, 1 skipped。pnpm exec turbo run type-check --concurrency=2→ 81/81 successful;node scripts/check-control-bytes.mjsOK;改动文件 eslint 0 errors(新增导出带的react-refresh/only-export-componentswarning 与本文件既有 8 个导出助手同类)。反向验证(先书面预判,commit 后变异,
git checkout --还原)changeField里的成员判定调用expected 'acme' to be 'Select value'—— C 独在时值可见但被错留,正是 B 的形态expected '' to be 'acc_007'/'acc_042';picker 的 id-input 那条按预判仍绿(另一个控件)['acme']而非['won'])hasStaticOptionDomain守卫,把远程列也接成员清空expected 'Select value' to be 'acc_007'。比预判多一条且性质不同:非数组options那条不是答错而是在optionKeysOf里抛异常(对字符串.map)—— 判据函数同时是该路径的全性保证。已把这个实测方向写在用例旁边,免得下一个人以为靠的是读取端的宽容Generated by Claude Code