refactor(app-shell): action-location 与目录查找两处手抄清单改为从 spec 派生 (#3017) - #3019
Merged
Conversation
…vocabularies from the spec (#3017) Two hand-copied spec vocabularies in the metadata-admin inspectors, each under a comment asking the next author to keep it in step with `@objectstack/spec` and nothing enforcing it. Both agreed with the spec today; neither had a mechanism making that true tomorrow. objectui#3017 catalogues the pattern. **`ActionDefaultInspector.LOCATIONS`** restated all seven `ACTION_LOCATIONS` values. The labels stay here — they are presentation — but the vocabulary is now `Record<ActionLocation, string>`, which makes the compiler the mechanism: a location the spec ADDS is a missing-key error (TS2741) instead of a silently absent dropdown entry, and one it REMOVES is an excess-property error (TS2353). Both directions verified against the installed spec. Display order is the object's insertion order, so the authoring-friendly grouping is unchanged. **`FlowReferenceField.KIND_TO_RECORD_LOOKUP`** hard-coded `object` / `valueField` for the four directory-backed kinds. Its own comment said to "import it once a published release carries the export" — `@objectstack/spec@17` does, so it now composes objectui's presentation on top of `APPROVER_VALUE_SOURCES`. That table is where this pattern has already cost us once: the FIRST copy of the data contract wired every directory kind to the metadata registry, which holds no `sys_user` / `sys_team` / `sys_business_unit` / `sys_position` ROWS, so candidates came back empty, the picker degraded to free text, and `sales_manager` got typed into a field that accepts three values (framework#3508). Spec answered by publishing the binding behind a `satisfies` that makes an undeclared `ApproverType` a compile error. Reading it here is what carries that guarantee across the repo boundary. The split the spec asks for is preserved: it owns WHERE candidates come from and WHAT is committed; this package owns `displayField` / `picker` / `subtitle`. A kind whose source is not `data` is skipped rather than thrown on — a module-level throw would white-screen the designer over a vocabulary change. No behaviour change: the derived table is byte-identical to the literals it replaces. `FlowReferenceField.specDerivation.test.ts` pins that it stays wired (non-vacuous, object/valueField sourced from the spec, presentation preserved). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
July 30, 2026 11:09
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…ks, and curate six (#3027) Seven record:* blocks shipped with renderers that read props but declared no inputs. That combination is the worst of both: the renderer honours `limit`, `severity`, `location` …, while every authoring surface — the designer panel, the AI vocabulary, the generated manifest — reports the block takes no configuration. #3013 recorded them as deliberately uncurated for exactly that reason. The declarations mirror what each renderer actually reads: record:activity 11 from RecordActivityComponentProps record:chatter 5 from RecordChatterComponentProps record:discussion 5 (same renderer, same inputs) record:alert 8 severity, title, body, visible, icon, action, dismissible, dismissKey record:quick_actions 7 actionNames, requiredPermissions, location, align, inline, variant, size record:history 3 limit, emptyText, unknownUserText record:reference_rail 1 hideEmpty inputs describe what an AUTHOR writes, which is a subset of what the renderer reads. `entries`, `loading` and resolved `actions` are injected by the host shell off RecordContext; declaring them would invite a model to hand-write the data the page is supposed to fetch. `aria` is omitted for the reason it is omitted on record:details — an accessibility escape hatch, not a layout choice. `location` takes its enum from the spec's ACTION_LOCATIONS rather than restating it, per #3019. Six of the seven join PUBLIC_BLOCKS: configurable and absent from the contract is the state #3006 was about. The contract goes 36 -> 42 tags, all resolving. record:chatter stays out — same renderer as record:discussion under a Salesforce-familiar name, kept for schemas already in the wild. Two spellings of one block is ambiguity an authoring model cannot resolve, so the vocabulary carries the spec's name. A test compares the two input lists, so the day they diverge the exclusion stops being justified and fails. A companion assertion requires every curated record:* tag to declare inputs. A curated tag with none reads as "takes no configuration" when the renderer in fact reads props — the same gap #3006 opened, pointed the other way. Claude-Session: https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Jul 30, 2026
Merged
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.
承 #3017(源自 framework#3786 的模式排查)。metadata-admin inspector 里两处手抄 spec 词表,各配一句「保持同步」注释、零机制。两处目前都与 spec 一致 —— 这不是 bug 修复,是把「碰巧一致」换成「不可能不一致」。
对应 framework 侧:objectstack#4120(那边同一模式已查出四份真的漂移过的表单,全部静默失效)。
1.
ActionDefaultInspector.LOCATIONS→ 编译期保证原先重述了
ACTION_LOCATIONS的七个值。label 留在本地(那是 presentation),但词表改由Record<ActionLocation, string>承担 —— 让编译器当机制:TS2741: Property 'x' is missingTS2353: 'x' does not exist in type两个方向都对着实际安装的
@objectstack/spec@17.0.0-rc.0验证过(构造最小样例跑tsc,确认 good 编译通过、missing/extra 各自报上表的错)。展示顺序仍是对象的插入顺序,所以 record → list → global 这个便于作者理解的分组没变 —— 没有被 spec 的声明顺序带跑。
2.
FlowReferenceField.KIND_TO_RECORD_LOOKUP→ 运行期派生 + 测试闸门原先为四个目录类 kind 硬编码了
object/valueField。它自己的注释就写着「import it once a published release carries the export」——@objectstack/spec@17已经带上了APPROVER_VALUE_SOURCES,所以现在真的 import 了,并把 objectui 的 presentation 组合在上面。这张表是本模式已经让我们付过一次代价的地方:第一份数据契约抄错,把每个目录类 kind 都接到元数据注册表上,而那里根本不存放
sys_user/sys_team/sys_business_unit/sys_position的行 —— 候选返回空、控件退化成自由文本框,于是sales_manager被打进只接受三个值的字段(framework#3508)。spec 当时的回应就是把 binding 发布出来,并用satisfies保证新增ApproverType不声明 binding 即编译错误。在这里读它,才是把那层保证跨仓传导过来。spec 要求的分工被完整保留:spec 管候选从哪来、提交什么值;本包管
displayField/picker/subtitle。source 不是data的 kind 走跳过而非抛错 —— 模块级 throw 会因为一次词表变更把设计器整个白屏。行为零变化
派生出来的表与它替换掉的字面量逐字段完全相同(已跑脚本比对)。
recordLookupFor的优先级不变:服务端发布的 binding 仍然优先,这张表仍是旧服务端的 fallback —— 只不过 fallback 的值现在来自编译进来的 spec,而不是一份手抄。测试
新增
FlowReferenceField.specDerivation.test.ts(7 个用例),盯的是「派生有没有悄悄断掉」:表非空、四个 kind 都在、object/valueField确实来自 spec、presentation 没在组合过程中丢失(尤其user的 people picker)。若 spec 把某个 kind 从data改走,测试会指名道姓地说「这个 picker 需要重新设计,不是重新派生一下就行」。验证
pnpm --filter @object-ui/app-shell type-check—— 通过(先做了完整 workspace build,否则@object-ui/*解析不到会报一堆假错)lint0 errors(包内 2054 条 warning 均为既有,我改的文件未新增)未处理
#3017 里另外四处(
object-fields-bridge↔FieldDesigner、RecordMetaFooter↔RecordDetailView、ImportWizard的两处 ↔ 服务端import-coerce)本 PR 未动 —— 它们要么是 objectui 内部的两份清单(需要本地断言当闸门),要么需要 framework 先导出常量才能派生。留在 issue 里跟踪。Generated by Claude Code