Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-grid,plugin-detail,plugin-list,core): 不物化列的字段类型不再给出排序头 (#3950) - #4965
Conversation
…ader from an unmaterialized column (#3950) A `formula` value is computed on read and no driver materialises a column for it, so a server `$orderby` naming one has nothing to order by. The sort never worked; until objectstack#6994 the platform answered `200` with the requested values unordered (asc byte-identical to desc on a real SQL driver), and it now answers `400 INVALID_SORT`. Either way a clickable header offers a sort that cannot be performed. `ObjectGrid` gated the affordance on the reference-bearing family alone (#3096). Unmaterialized types are a second reason a server sort is impossible, not a different mechanism, so the same seam now reads both — as do the two sort entry points of a related list, which derive the rule separately (measured: the `sortable` derivation is NOT shared across the data-table renderers). Membership moved out of a private set in `ListView` into `@object-ui/core` (`UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType`), bound to `@objectstack/spec`'s own storage predicate rather than restated, and pinned narrower than the spec's write contract: `summary` and `autonumber` have real columns and keep their headers. Client-side sorting is untouched (the key is the value the cell shows, which is honest), and a sort declared in view metadata is still sent and still refused by name rather than silently dropped. Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…materialized rule (#3950) Both packages document the relational carve-out; the same paragraphs now carry the second reason a server sort is withheld. `plugin-list`'s closing sentence also had to be corrected rather than extended: it claimed column-header sorting "is unaffected: it is client-side over the rows already loaded". That stopped being true at objectui#3106, which made a header click a server `$orderby` whenever the grid shows one window of a larger collection — so the sentence asserted the opposite of the rule this change applies there, and the two errors are the same sentence. The client-side half is kept and stated where it actually holds (inline data and the grouped view, which holds every row it groups). Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 17, 2026
PM 验收 ✅ ACCEPT(#3950,批次 19) 实物核验(按真实 merge-base 对账):12 files,+669/−72 —— 三处入口收敛(ObjectGrid CI 亲读:20/20 check runs completed,零失败(dependabot/coverage 两项 path-filter skipped,计绿)。 反向验证读数:变异 2 预判 1 红实测 3 红 —— 方向一致,多出的两条是 corpus 两处真实标本(CRM undraft + auto-merge(squash)。 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#3950
结论先说
formula列的排序头是修改前后都错的:它提供一个平台无法执行的排序。framework#6994 之前平台不说话(200,返回的正是它被要求据以排序的那些值,但没排序 —— 真实 SqlDriver 上 asc 与 desc 逐字节相同);之后是400 INVALID_SORT。所以这个修法不依赖 pin 推进,无 Blocked-by。卡面两个开放验证点(先做,结论如下)
① RelatedList 等 data-table 渲染器是否共享 sortable 派生路径 —— 不共享。 服务端排序的 affordance 判定在三处各自调用
isExpandableFieldType,谁也不读谁:packages/plugin-grid/src/ObjectGrid.tsxwithSortabilitymanualSortingOnpackages/plugin-detail/src/RelatedList.tsxsortableColumnswindowedpackages/plugin-detail/src/RelatedList.tsx排序按钮行data-list才有)windowedsort-values.ts的文档注释其实已经记下了这个状态:「surfaces that sort on the server must instead not offer relational columns as sort keys (that half of #3096 lives at each entry point)」。第四处是ListView工具栏的排序选择器,它已经排除 formula(#4243),但用的是自己文件里的一份私有集合。派生路径不共享,所以三处都改;并且把成员集合上提到
@object-ui/core,让四处读同一个判断。ListView渲染的正是type: 'object-grid',所以 ObjectGrid 一处即覆盖 ListView 的表头与独立 ObjectGrid 两个入口。② corpus 里有没有 list view 真渲染 formula 列 —— 有,两处,所以这是今天就用户可见的缺陷:
examples/app-crm/src/views/opportunity.view.ts的默认columns与listViews.all.columns都含{ field: 'expected_revenue' },而expected_revenue是crm_opportunity上的Field.formula(opportunity.object.ts:51);examples/app-showcase/src/ui/views/project.view.ts:24的columns含{ field: 'budget_remaining' },是 showcaseproject上的Field.formula(project.object.ts:65)。两者都真的会渲染出值(
isProjectableField只看字段是否声明,formula 已声明 → 进$select,服务端 on-read 水合),所以是「有值、可点、点了没用/报错」。即:CRM 的 Opportunities 列表、showcase 的 Projects 列表,点「Expected Revenue」/「Budget Remaining」表头 —— pin 推进前是静默的无效点击,推进后是用户可点出来的 400。改法
一个门,两个理由,同一个机制。列被当作排序键的前提是两条同时成立:
lookup/master_detail/user/tree):存的是外键 id,显示的是关联记录名;服务端只能按 id 排(objectstack#4256 已定不做 join)。formula):没有任何驱动为它物化列,压根没有可排的键。成员集合从
ListView的私有拷贝搬到@object-ui/core(UNMATERIALIZED_FIELD_TYPES/isUnmaterializedFieldType),并且绑定到@objectstack/spec自己的存储事实而不是在这里重述一遍 —— 平台的 filter 门对自己用的就是这个理由(「so this gate and the drivers cannot disagree about which types have a column」)。重述会漂移,漂移的两个方向都坏:要么收掉一个能用的 affordance,要么留一个会 400 的。刻意窄于 spec 的写入契约
COMPUTED_VALUE_TYPES(formula/summary/autonumber):summary和autonumber各有真实维护的列、排序正常,按写入契约收会误伤两个能用的类型 —— 平台自己的 conformance 用例点名钉了这个坑。测试里两边都钉住。刻意不改的两件事
manualSortingOn/windowed)。validate-searchable-fields),不在本 PR 范围。文档(AGENTS.md #2)
两个包的 README 都有「Sorting」小节在讲这条规则,一并跟上:
packages/plugin-detail/README.md—— 原本准确描述了 relational 那一半,补上第二个理由,并点明这条规则作用在本卡片的两个排序入口上。packages/plugin-list/README.md—— 这一段不是「补充」而是纠正,请 reviewer 特意看一眼:它原先写着 column-header sorting 「is unaffected: it is client-side over the rows already loaded」。这句自 objectui#3106 起就不成立(窗口化时表头点击本身就是服务端$orderby),于是它恰好断言了与本次改动相反的事实 —— 两个错误是同一句话,无法只改一个。客户端那一半保留,但挪到它真正成立的地方(inlinedata、以及持有全部行的 grouped 视图)。测试
新增/改动 27 个断言(全绿):
packages/core/src/utils/__tests__/unmaterialized-fields.test.ts—— 成员恰好是formula;引用同一性(不是值相等:忠实拷贝写下的当天就通过一切值比较,之后静默漂移);summary/autonumber反向钉住;与EXPANDABLE_FIELD_TYPES互斥(两个理由若重叠,消费端会有一支变成死代码,且两者给的补救建议不同)。packages/plugin-grid/src/__tests__/ObjectGrid.unmaterializedSort.test.tsx—— formula 表头无排序 affordance;点它不产生任何 refetch(所以没有请求给平台去回 400);误伤对照:text/currency/percent/autonumber/summary五列逐个真点表头并断言$orderby发出;客户端模式下 formula 表头保持可点。packages/plugin-detail/src/__tests__/RelatedList.unmaterializedSort.test.tsx—— 两个排序入口各自钉住(表头 +data-list的按钮行),各带 stored 列对照与客户端模式对照。packages/plugin-list/src/__tests__/ListView.relationalSort.test.tsx—— [console] List sort: filterableFields doubles as the sort whitelist, and a header click discards the view's multi-level default with no way back #4243 的 formula 规则此前只有文字描述、没有行为钉子(fixture 里根本没有 formula 字段)。往共享 fixture 加一个,该文件原有的精确相等断言就顺带变成了这条规则的钉子。反向验证(先预判后跑)
变异 1 —— 把 ObjectGrid + RelatedList 两处新加的门撤回(保留 ListView)。预判:恰好 4 红(grid 的表头与点击、related list 的表头与按钮),8 个误伤对照与 2 个客户端模式全绿,ListView 选择器全绿 —— 因为那条规则先于本 PR 存在。实测完全一致:
Tests 4 failed | 23 passed。变异 2 —— 删掉 ListView 选择器里的第二条规则。预判 1 红,实测 3 红,方向一致而数量更多:多出来的两条是该文件原有的精确相等断言(
toEqual(['Name','Amount'])、toEqual([…,'Owner (by ID)'])),因为score进了共享 fixture,它们免费变成了这条规则的钉子。一处照直说明,不套模板:变异 2 下
keeps a formula field the current sort already names仍然绿。这是可预期的 —— 它断言的是「被保留」,而规则被删掉后该字段本来就不会被收掉,所以它是因为什么都没发生而通过。单独看它不承重;承重的是紧邻的 withholding 断言,两条合起来才区分「被例外保留」与「从未被收掉」。保留它是为了钉住例外的标签形状(不带 by-ID 后缀 —— 那是关于关系键的陈述,这个字段压根没有键可描述)。变异都在 commit 之后做、用
git checkout --还原(不用 stash),还原后重跑 27 全绿。门与检查
changeset:
.changeset/formula-column-sort-header-3950.md(patch × 4 包)。相邻发现,均已立卡、未认领,不在本 PR:objectstack#9257(SORT 轴缺 authoring gate)、objectui#4966(
ListView.tsx一个 docblock 错位)、并在 objectui#4559 上留了坐标漂移说明(它的取证正落在本 PR 改写的那个注释块里,且它本身未被本 PR 修掉)。