Skip to content

fix(components): FilterBuilder 切换 field 时按新列类型重整 value —— 可转则转,不可转清空 (#4781) - #4876

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4781-filter-value-reconcile
Aug 16, 2026
Merged

fix(components): FilterBuilder 切换 field 时按新列类型重整 value —— 可转则转,不可转清空 (#4781)#4876
yinlianghui merged 2 commits into
mainfrom
claude/issue-4781-filter-value-reconcile

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4781

按维护者 2026-08-16 裁定(评论 5306092839,方案 B:能转则转、不能转才清)实施。

前提复核

按 origin/main 65e88e6 逐条核对,issue 正文的读数全部成立,PR #4779 落地后此处代码未再动过:

  • changeField 在 operator 未变时走 { ...c, field: nextField },value 原样带过去;
  • 值输入框由 getInputType字段类型重绘,type="number" 的输入框对非数字 value 显示空白;
  • 已有测试 filter-builder-field-switch-operator.test.tsx 里那条「a scalar value is carried through untouched」正是把这个行为钉住的(text 的 "acme" 切到 number 列后仍是 "acme")—— 缺陷有现成的钉子,实测通过。

改了什么

切换 field 现在是一次编辑,同时结算三件事:operator(#4768 已做)、value 的形状reshapeFilterValue#4768 已做)、value 的类型retypeFilterValue,本单新增)。

convertibility 判定收在一处 convertScalarToFamily,与 reshapeFilterValue 并列,判定标准是干净、无歧义的读数

目标族
number "42"42" 42 "42"-3.5""1e3" "acme""42abc""1,000""Infinity"true
boolean "true" / "False" → 布尔(两端可往返) 1"yes""acme"
date "2024-03-05" 原样;"2024-03-05T14:30""2024-03-05" "2024-02-31""05/03/2024"、带时区的 ...Z / ...+08:00
datetime 无时区的 "2024-03-05T14:30[:ss]" 原样 裸日期、"...T25:00"、带时区
time "14:30[:ss]" 原样 "29:71"、时间戳
text / select / lookup / 未知 全收;非字符串走 String()42"42"true"true"

几处刻意的取舍,都写进了代码注释:

  • parseFloatparseFloat 会把 "acme" 读成 0"42abc" 读成 42,那是用户从未写过的筛选,正是裁定要挡的。文件里另外三处键入路径仍是宽松写法,今天被数值输入框挡着碰不到 —— 单独立了观察类 issue FilterBuilder 三处数值输入用 parseFloat(raw) || 0 —— 半截读数与 NaN 都落成 0(今天被 number 输入框挡着) #4875,不在本 PR 扩围。
  • 裸日期不会被补上午夜去凑 datetime 列equals 2024-03-05T00:00 是一条看起来已填、实则几乎匹配不到东西的规则,比空输入框更坏。
  • 无时区时间戳切到 date 列做截断:这是唯一一处不丢「目标输入框本来就能显示的信息」的转换,而且它让显示与存储重新一致(formatValue 早就按 T 切开显示了)。带时区的一律清 —— 手工截断正是裁定说要拒绝的歧义。
  • 空标量 '' 不动:没填的行本来就是该族的空形,「清空」它是把无操作装成决定。

顺带把 getInputType 改为读同一张 family 表,而不是对同一批类型列表再写一遍分支 —— 「值被转成的类型」与「值被编辑的输入框」是同一个问题,答两遍正是二者能各说各话的原因,而不一致就是本单的病灶。行为不变,按类型逐条钉住。

测试

新增 filter-builder-field-switch-value.test.tsx(38 条):UI 驱动真实下拉的转换/清空/不动三类,加上 retypeFilterValue 的逐族矩阵与逐形状(scalar / list / pair)矩阵。list 分支老实标注了可达性 —— 今天只有 select / lookup 两个桶提供 in/notIn,二者同属文本族,所以没有任何字段切换能把 list 驱到数值列,该分支按「算子落到哪一族」写并直接钉在 helper 上。

既有 fixture 的两处分诊filter-builder-field-switch-operator.test.tsx):

  • 「a range keeps its lower bound」:目标由 Amount 改为 Title。它钉的是 pair → scalar 的塌缩,text 列同样触发算子重置(其桶里没有 between)且装得下塌缩出来的日期串;留在数值列上会让这条钉子被一个不属于它的值判定盖住。
  • 「a scalar value is carried through untouched」:目标由 Amount 改为 Stage。它原本钉的就是本单裁定要推翻的行为("acme" 活着落到 number 列);钉子本身的命题(形状重置不该抹掉值)不变,故换到一个仍装得下该值的目标,number 目标的相反判定进了新测试文件。

命令与结果:

pnpm exec vitest run packages/components --maxWorkers=2
  → Test Files 143 passed (143) | Tests 1281 passed (1281)

pnpm exec vitest run <消费半径:app-shell viewFilterFold ×4 / plugin-list ×2 / fields / plugin-view>
  → Test Files 8 passed (8) | Tests 209 passed (209)

pnpm exec turbo run type-check --concurrency=2
  → Tasks: 81 successful, 81 total

node scripts/check-control-bytes.mjs
  → OK (scanned 4356 tracked text file(s))

反向验证

先提交,再把 changeField 里的 retypeFilterValue 调用断开(退回 value: reshaped)。预判先写死:15 条转红 —— 复现两条、it.each 里四个目标装不下的切换、5 条转换钉、4 条清空钉;而 6 条「新列装得下就别动」的爆炸半径守卫、10 条 helper 矩阵、11 条输入框类型钉、以及整个 operator 测试文件保持绿。实跑 15 failed | 48 passed,逐条对上。

第一轮反向验证还揪出我自己测试里的一条假钉子:原先写的「输入框显示的与行存的一致」是 expect(input.value).toBe(String(row.value)),断开后它仍然绿 —— jsdom 对「新建即为数值输入框」会做值净化(显示空),但对原地翻转 type(字段切换正是这个动作)不会重跑净化,于是它把切换前那个浏览器本该抹掉的字符串原样交回来。这条钉子不可能因它被写下的理由而失败,等于没钉。已替换为直接断言「行里存的值是该输入框能原样显示的值」,并把这段测量写进注释;同时补了一条「数值列对非数字值显示空」的症状钉(两个方向都绿,作用是:值控件哪天不再是数值输入框,清空规则就该重新想,而不是静默守着空气)。

变更集

.changeset/filter-builder-field-switch-retypes-value.md@object-ui/components: patch)。

顺带发现(均已立单,未在本 PR 修)


Generated by Claude Code

…4781)

切换一行的 field 时,PR #4779 只在算子家族变化时重整 value 的形状;标量到
标量没有形状问题,值被原样带过去。但字段的**类型**也换了,而值输入框正是
按新类型重绘的:`<input type="number">` 对非数字 value 一律显示为空。于是
text 列的 `equals "acme"` 指到 number 列后,输入框空白,行却仍带着
`"acme"` —— `foldFilterGroupToSpecRules` 原样持久化,实时网格拿
`amount equals "acme"` 去查。与 #4768 同一形状的「看不见的值」,只是发生在
value 而不是 operator 上。

按维护者 2026-08-16 裁定(方案 B:能转则转、不能转才清):

- convertibility 一处定义(`convertScalarToFamily`),与 `reshapeFilterValue`
  的 carry-if-possible 语义并列;判定为**干净、无歧义**的读数,比
  `parseFloat` 严格 —— 后者会把 `"acme"` 读成 `0`,写出用户从未写过的筛选;
- number:`"42"` → `42`,`"42abc"` / `"1,000"` / `Infinity` 清空;
- boolean:只认 `"true"` / `"false"`(两端可往返),`1` / `"yes"` 是约定不是
  读数,清空;
- date/datetime/time:只收目标输入框本身能渲染的值,外加唯一一处不丢信息的
  截断(无时区时间戳 → 其日期)。裸日期不会被补上午夜去凑 datetime 列:
  `equals 2024-03-05T00:00` 是一条看起来已填、实则几乎匹配不到东西的规则;
- 新列装得下的值原样保留(两个 text 列之间、两个数值列之间),空行仍是空行。

`getInputType` 改为读同一张 family 表,使「值被转换成的类型」与「值被编辑的
输入框」不可能各说各话 —— 二者不一致正是本单的病灶。

Co-authored-by: Claude <noreply@anthropic.com>
`FILTER_INPUT_TYPE_BY_FAMILY` 上一版标了「@internal exported for tests」,
但测试并不导入它 —— 输入框类型是从渲染出来的 DOM 上钉的,那本来就是更硬的
钉法。`packages/components/src/custom/index.ts` 是 `export *`,于是它白白进了
包的公共 API(连 `dist/*.d.ts` 都带上了)。没有消费方要求的东西不进契约。

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-DPjW1tE7.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.36KB 111.03KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 157.05KB 43.28KB
fields (index.js) 231.73KB 57.60KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.86KB 10.83KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.44KB 18.28KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.14KB 32.58KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 239.81KB 59.97KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 115.34KB 27.74KB
plugin-gantt (index.js) 164.06KB 39.94KB
plugin-grid (index.js) 192.23KB 51.30KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.23KB 26.97KB
plugin-map (index.js) 17.91KB 5.72KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

【PM 验收 · ACCEPT】objectui 分片 PM(session_01GTRjn8xBqp75dk7kFupVRt)对 #4781 裁定(方案 B,评论 5306092839)的实施验收:

实物核验:4 文件对账相符(filter-builder.tsx +269、新钉 38 条 + 旧文件 2 fixture 再分诊、changeset);边界零越包、模型标识 0、releases 0。裁定逐条兑现:convertibility 一处定义(convertScalarToFamily,严于 parseFloat —— "acme" 不再读成 0);date/boolean 干净无歧义才转,默认清空;唯一不丢信息的截断(datetime→date)与「裸日期不补午夜」的不对称取舍已在 PR 声明理由;机制复用 #4768 的 reconcile 家族形态。结构性加分:getInputType 改读同一张 family 表 —— 值被转成的类型与值被编辑的输入框结构上不可能各说各话,正中病灶。

反向验证:15 红 48 绿逐条命中;更可贵的是第一轮揪出自己的假钉(jsdom 对原地翻转 input type 不重跑值净化,原断言不可能为其被写下的理由失败)—— 测量写进注释、换成 inputShowsExactly 并补两向皆绿的症状钉。conversion matrix 全族覆盖(含 list/pair 形状与可达性如实标注)。CI 亲读:19 项全 completed(17 success + 2 skipped),零失败。

三个新 finding(#4873 假值当空、#4874 值域外显示空需裁定、#4875 三处 parseFloat||0 口径并存)记账合格,留分诊。

处置:undraft + auto-merge(SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 16, 2026 20:01
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit be60815 Aug 16, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4781-filter-value-reconcile branch August 16, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FilterBuilder 切换行的 field 后 value 原样留下 —— 文本值落到 number 列,输入框显示空而行仍带着该值

2 participants