fix(app-shell): 运行时工具栏保存筛选前把 FilterGroup 折成 spec 的 ViewFilterRule[] - #3360
Merged
Conversation
… before persist
WIP checkpoint — implementation complete, verification pending.
objectstack#5159: the list toolbar's `Filter -> Add filter -> save` PUT the
FilterBuilder's whole `FilterGroup` ({id, logic, conditions}) into the view's
`filter`, where `ListViewSchema.filter` declares `z.array(ViewFilterRuleSchema)`.
The server answered 422 `invalid_union` on every save.
Fix the PRODUCER (AGENTS.md #0.1) — the spec is untouched, no union widening:
- New `packages/app-shell/src/views/viewFilterFold.ts` holds the WRITE half of
the bridge whose READ half is plugin-view's `parseSpecFilter`/`toFilterGroup`.
Extracted from the Studio inspector's `FilterBuilderField`, which had the only
copy — which is exactly why the runtime toolbar had none.
- Operators normalize through the spec's own `normalizeFilterOperator` instead
of the local `FB_TO_SPEC` table that copy carried; that table had drifted four
builder operators behind (startsWith/endsWith/isNull/isNotNull).
- Row `id` is stripped: the read path regenerates it (`parseSingleOrNested` does
`item.id || crypto.randomUUID()`), so the at-rest body stays the declared
{field, operator, value} vocabulary only.
- A1 adjudication: a group that cannot fold losslessly — `logic: 'or'` across
2+ conditions, or a nested group — is REFUSED with a translated toast and not
saved. Never silently downgraded to AND. `logic:'or'` over <2 rules folds,
because there OR and AND select the same records.
- localStorage keeps the builder's group verbatim (it rehydrates the toolbar);
only the spec-governed view body is folded.
Remaining steps:
1. app-shell + i18n test/lint/typecheck runs
2. changeset
3. draft PR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Adds replay-matrix variant ② to `viewFilterFold.test.ts` and records the
measurement in the fold's contract docs.
The issue's matrix recorded variant ② (flat `ViewFilterRule[]` still carrying
the FilterBuilder's row `id`) as ACCEPTED — but that was measured against a
server running framework `main`, where #5154 tolerates the undeclared key.
Measured against the spec objectui itself pins (17.0.0-rc.2),
`ViewFilterRuleSchema` is a `strictObject` over `{field, operator, value}` and
rejects it with `unrecognized_keys` on `filter.0`.
So stripping `id` is not merely the tidier at-rest shape — under this pin it is
required, and a fold that kept it would have traded a 422 `invalid_union` for a
422 `unrecognized_keys`. The new tests pin both that rejection and the fact that
the fold strips the key, so a later spec bump that declares `id` shows up here
rather than as a silent behaviour change.
Refs: objectstack-ai/objectstack#5159
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
修复 objectstack-ai/objectstack#5159
维护者裁决 A1:修生产者 —— objectui 在持久化前把 filter-builder 的
FilterGroup折叠成 spec 声明的ViewFilterRule[],复用 Studio 侧已有的同一变换;logic: 'or'与嵌套分组响亮拒绝,⛔ 绝不静默降级成and;spec 零改动,不放宽 union。问题
列表工具栏
Filter → Add filter → 保存时,onFilterChange的载荷被原样交给persistViewPatch({ filter })。那个载荷是 filter-builder 自己的分组方言{ id, logic, conditions },而ListViewSchema.filter声明的是z.array(ViewFilterRuleSchema)—— 每次保存都是422 invalid_union,筛选条件从未真正落盘。因为 objectstack#5014 把 union 报错压平成不含filter字样的Invalid input,它在 main 上一直没被发现。改动
新增
packages/app-shell/src/views/viewFilterFold.ts,承载写方向的折叠(读方向parseSpecFilter/toFilterGroup早已存在于 plugin-view)。该变换从 Studio 侧检查器metadata-admin/widgets.tsx的FilterBuilderField提取而来 —— 那里原本是唯一一份拷贝,正因如此运行时工具栏一份都没有。现在两处走同一个函数,不再是两套方言。normalizeFilterOperator归一。widgets.tsx 原来那张手维护的FB_TO_SPEC表已随之删除:它比 builder 下拉框落后了四个算子(startsWith/endsWith/isNull/isNotNull),这些此前会被原样写进库里。spec 不认识的算子原样透传,让服务端 enum 响亮拒绝,而不是被强行coerce 成equals。Add filter在用户选列前插入的field: ''行)。id剥离,见下。logic: 'or'/ 嵌套分组 —— 响亮拒绝(A1)无法无损折成扁平 rule 列表的形状拒绝保存,并通过
toast.error(t(...))给出 i18n 文案(10 个语言包均已补齐console.objectView.filterOrNotSavable/filterNestedNotSavable)。理由:把 OR 悄悄按 AND 写回去,存下来的视图会返回与用户屏幕上不同的记录集。该筛选仍然作用于当前列表(convertFilterGroupToAST在会话内尊重logic),只是不进入保存的视图。边界:
logic: 'or'在少于两条有效规则时折叠而非拒绝 —— 0 或 1 条时 OR 与 AND 选出完全相同的记录,没有任何东西可丢失。拒绝从语义真正分叉处开始。id的去留 —— 实测后剥离议题给的回放矩阵里变体 ② (
rule[]保留id) 记为 ACCEPTED,但那是打在跑 frameworkmain(#5154 容忍未声明键)的服务端上。对着 objectui 自己 pin 的 spec(17.0.0-rc.2)实测,ViewFilterRuleSchema是strictObject({field, operator, value}),带id的规则被unrecognized_keys拒在filter.0:所以剥
id不只是「落盘形状更干净」,在本仓的 pin 下它是必须 —— 保留只会把一个 422 换成另一个 422。另一独立理由:读方向本就重新生成 id(parseTriplet无条件crypto.randomUUID(),parseSingleOrNested/toFilterGroup兜底),builder 往返无损。两条都已入测试。验证
验证层级:payload-boundary + 单元(未驱动真浏览器)。折叠产物直接对服务端所用的同一个
ListViewSchema断言,回放矩阵三变体在测试里闭合:FilterGroupListViewSchemaREJECTED,且失败路径确为filterrule[]保留idfilter.0/unrecognized_keysrule[]去id(折叠产物)另有
view-filter-fold.ratchet.test.ts守住持久化路径本身:对变换做形状断言,若将来某次改动把裸 group 绕过它就一文不值。该 ratchet 已反向验证 —— 手工把persistViewFilter改回persistViewPatch(…, { filter })后,两条断言如期失败(this onFilterChange bypasses the fold),恢复后复绿。已含 changeset(patch:
@object-ui/app-shell/@object-ui/i18n)。未触碰 releases 文档。🤖 Generated with Claude Code
https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Generated by Claude Code