Skip to content

feat(app-shell)!: modal target 只命名 page,退役 object fallback 与前缀约定 (#3925) - #4764

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3925-retire-modal-object-fallback
Aug 16, 2026
Merged

feat(app-shell)!: modal target 只命名 page,退役 object fallback 与前缀约定 (#3925)#4764
yinlianghui merged 1 commit into
mainfrom
claude/issue-3925-retire-modal-object-fallback

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Fixes#3925

按维护者裁定 objectstack#6739-A(2026-08-09,decision-inbox 轮次「全部接受」):type: 'modal' 动作的字符串 target 只命名 PAGE。本仓的 page-then-object 解析属于消费端宽容,退役。

Base: 2646ccb728e36af1e7b38d79de49c09f041e4ed6

改了什么

packages/app-shell/src/hooks/useActionModal.tsx:

  1. resolveModalTarget —— 删掉 page 未命中后的 object 元数据回落。非 page 名是拒绝,不再静默解析为 object。
  2. create_/new_/add_/edit_/update_ 前缀约定 —— 随同退役。normalizeModalSchema 对字符串一律返回 { targetName },不再拆成动词 + object。裁定明确拒绝了第三形态(保前缀、弃裸名)。
  3. 诊断 —— modalHandler 点名被拒 target 并给出替代:Modal target "contact" names no page — a modal action's `target` names a PAGE, only. To open an object's form, use `type: 'form'` with an `object.view` target.
  4. ModalDescriptor.objectName 的 "Back-compat" 注记(交付清单第 3 项)—— 重判为「保留,但注记是错的」。理由见下。
  5. 顺带纠正 resolveModalTarget 一段过期 TSDoc:它仍写着 null 会回落到服务端处理器,而该回落早已由 objectstack#3959 / objectui#3320 移除。这段在 main 上就已与同文件消费者的实际行为矛盾。

为什么 objectName 保留而不是一并删

不是 back-compat,也不是宽容 —— 它的唯一在产者是 lookup 字段的行内「创建被引用记录」:@object-ui/fieldsLookupField 调用 execute({ type: 'modal', modal: { objectName, mode } }),传入的是完整 descriptor,object 身份来自字段的 referenceTo,而不是从名字猜出来的。这条路径与动作的 target 无关,因而不在裁定退役范围内。真正退役的是推断:字符串 target 不再能变成 objectName。注记已按此改写。

风险门(卡内明示:发现真实依赖 → 停手报测量)—— 已枚举,未发现依赖,放行

位置形态是否依赖被删分支
objectstack examples/app-showcaseQuickViewActiontype: 'modal' + target: 'showcase_component_gallery' —— 目标是已声明 page,走 page 分支
objectstack showcase 「Create Task」行内按钮已迁至 type: 'form' + showcase_task.edit —— os#6737 已落地,实测确认
objectstack examples/app-showcase/test/actions.test.ts语料钉:每个 modal target 必须是已声明 page —— 与本变更同向
objectui apps/examples/全仓无 type: 'modal' 动作携带字符串 target(命中项均为 FormView 的 type: 'modal',另一个概念)
objectui packages/fieldsLookupField.tsx:823{ objectName, mode }descriptor,非字符串 target —— 见上,保留且有测试钉
objectui packages/app-shell/.../metadata-admin/package-schema.ts:81FormViewSpectype: 'modal'(表单呈现模式) —— 不同概念
全仓前缀约定 target 元数据

测试

packages/app-shell 全量:403 文件 / 3831 通过 | 1 skipped

按三类处置逐条重判 fixture,而非批量改写:

  • 整体替换(原用例正好钉住被删的那条肢体):「falls back to an object create form」→ 改为拒绝钉;「still honors the create_ prefix」→ 改为拒绝钉(与裸名同判,即钉 ③)。两个拒绝钉的 fixture 都故意让 object 真实存在于元数据里 —— 对空 object 列表断言 null 会空洞通过,只证明没找到,而非证明没去找。同时断言 getItem 从未以 ('object', …) 被调用。
  • 改写(原用例断言的是被删语义):useActionModal.test.ts 的两条前缀用例改为断言前缀被解析;新增一条「前缀名与裸名判定完全一致」,直接钉裁定拒绝的第三形态。
  • 保留为对照:page 解析、{ objectName, mode } descriptor 直通(LookupField 路径)、缺失 target 的独立诊断。

反向验证 —— 先预判,后跑变异

预判(恢复两条被删肢体后):3 条 normalize 钉转红、2 条拒绝钉转红、诊断钉以超时而非断言转红(因为 target 一旦可解析,modalHandler 会打开对话框并返回一个只在关闭时 settle 的 promise),5 条对照保持绿。

实测:7 红 8 绿,比预判多一条。多出来的是诊断钉的邻居「reports a missing target distinctly」—— 单独隔离跑(-t)在变异下通过,证明它是被上一条超时用例污染的级联失败,不是独立信号。

这条不写进报告了事:我据此加固了诊断钉 —— 在调用 modalHandler 前先断言 resolveModalTarget 返回 null。这样一旦回落复活,用例会立刻以干净断言失败,而不是挂到超时再把状态泄漏给邻居。

type: 'form' 对照(交付清单第 4 项)诚实说明:该路径由 packages/coreActionRunner.executeForm 承接(导航至 /forms/ + 表单名),与 useActionModal无任何共享代码,结构上不可能被本变更影响 —— 所以我没有新造一条形式上好看的"form 钉",而是把既有 ActionRunner.formObjectIdentity.test.ts + ActionRunner.test.ts 作为对照跑通(6 文件 / 159 通过)。

门禁

  • turbo run type-check --concurrency=281/81 successful
  • node scripts/check-control-bytes.mjs → OK(4277 文件);另按纪律自扫改动文件的控制字节,干净
  • check:action-forward-parity / check:spec-symbols / check:phantom-deps → 通过
  • eslint(改动文件)→ 0 error(25 条既有 any warning)

超范围发现(未立单,按派发词回传 PM)

  1. 两处诊断文案已随本变更过期:useConsoleActionRuntime.tsx:600RecordDetailView.tsx:883 都写 names no page or object to open,且只指向 type:'script',不提 type: 'form'控制台动作的实际用户可见文案出自这两处,而非 useActionModal.modalHandler。按边界(views/ 属 [finding] 动作对话框的继承选项谓词只认外层页面的 record:自己在途的 param 值不参与,select 的 dependsOn 门根本到不了 #3765 面)未改。建议单独一张卡统一三处文案。
  2. DashboardView.tsx:70-93 自带一份独立的前缀约定实现,不经过 useActionModal,因此本次退役影响不到它 —— 但这意味着同一个已被裁定退役的约定在仓内仍有第二份活体,注释还自称支持「server-driven dashboard schemas」发出的 verb_object。属 views/ 面,未改。

按维护者裁定(objectstack#6739-A,2026-08-09):`type: 'modal'` 动作的字符串
`target` 只命名 PAGE。`resolveModalTarget` 原先 page-first 之后回落到 object
元数据,并附带 `create_`/`new_`/`add_`/`edit_`/`update_` 前缀约定,把
`create_opportunity` 解析成 object `opportunity` 的 create 表单。
两者一并退役。契约本身从无歧义:spec TSDoc、已发布文档与 `defineStack` 的
交叉引用遍历都规定 modal target 是 page,且该遍历会拒绝 target 非声明 page 的
已注册 modal 动作。回落属于消费端宽容 —— 让运行时供给构建门禁所拒绝的东西,
把编写错误变成"看起来能用",语料也因此学到错误形态。裁定明确拒绝了第三形态
(保前缀、弃裸名)。
非 page 名现在是拒绝,诊断点名 target 并指向 `type: 'form'`。
同 PR 重判 `ModalDescriptor.objectName` 的 "Back-compat" 注记:保留该键,但注记
是错的 —— 它的唯一在产者是 lookup 字段的行内"创建被引用记录",传入的是完整
descriptor,object 身份来自字段的 `referenceTo`,与动作 target 无关。已改写为
descriptor-only 键的说明。
顺带纠正 `resolveModalTarget` 中一段过期 TSDoc:它仍在描述 `null` 会回落到
服务端处理器,而该回落早已由 objectstack#3959 / objectui#3320 移除。
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercelBot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 16, 2026 4:35am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-B56Ze6mM.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)490.79KB108.95KB
core (index.js)3.79KB1.52KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)157.05KB43.28KB
fields (index.js)231.21KB57.42KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.96KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.89KB1.22KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.86KB12.91KB
plugin-charts (index.js)64.44KB18.28KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.14KB32.58KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)239.81KB59.97KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.64KB27.69KB
plugin-gantt (index.js)164.30KB40.02KB
plugin-grid (index.js)192.10KB51.24KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.23KB26.97KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.38KB11.09KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.64KB9.44KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收(座位 repo:objectui,session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3925 按维护者裁定(os#6739-A)落地 —— modal 字符串 target 只命名 page,object fallback 与前缀约定退役,非 page 名走点名诊断指向 type:'form'。

核验记录(实物核验 + CI 亲读):

  1. 风险门先测后删:双仓依赖枚举 —— showcase 语料已按 sequencing 迁 type:'form'(os#6737 于 objectstack main 实证)、objectui 零字符串 modal target(FormView 的 type:'modal' 是另一概念、LookupField 的 descriptor 传参路径结构不同并留钉)、前缀约定零消费者。删除有据。
  2. fixture triage 逐例处置:钉住被删分支的两例整体替换且刻意保持 object 在场(null 断言非空洞 + getItem 反调用断言);新增「前缀名与裸名同判」钉正是裁定拒绝的第三形态。
  3. 反向验证的多红一处处理到位:7 红 vs 预判 6 红,隔离重跑证明多的一红是前钉超时的级联污染;不止上报还加固了诊断钉(先断 null 再进 modalHandler),复活的 fallback 现在快速干净地红。form 对照用既有 ActionRunner 测试(结构免疫)而非造假钉,诚实。
  4. changeset minor 有先例(dirty-pumpkins-shout 的行为移除先例),破坏面写明,ModalDescriptor.objectName 重判保留且 back-compat 标注更正;顺手修正了与消费者矛盾的过期 TSDoc(in-scope)。
  5. 门与规程:app-shell 3831 绿;type-check 81/81;action-forward-parity/spec-symbols/phantom-deps 绿;fable 0;releases 与 views/ 零触碰(边界遵守,实物 diff 确认)。
  6. CI 亲读终态:20/20 completed、0 失败(shard×4 至 04:43:54Z)。

out-of-scope 已由 PM 落位:#4766(DashboardView 前缀约定第二副本,queue)、#4767(三处诊断文案统一,queue)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 16, 2026 04:56
@yinlianghui
yinlianghui added this pull request to the merge queueAug 16, 2026
Merged via the queue into main with commit 171601cAug 16, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3925-retire-modal-object-fallback branch August 16, 2026 04:57
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 17, 2026
…jectstack-ai#4780)
`type: 'modal'` 动作的字符串 `target` 指不到页面时,三处会拒绝并报错:
`useActionModal.modalHandler`(ActionProvider `onModal` 路径)、
`useConsoleActionRuntime.modalActionHandler`(列表页 / SDUI 页 / 声明式动作栏)、
`RecordDetailView.modalActionHandler`(记录页)。三处各自手写文案,于是漂移了。
PR objectstack-ai#4764 退役了 object fallback(modal target 只指页面 —— objectstack#6739 的
维护者裁定),并只改写了 `useActionModal` 一处:点名被拒的 target,指向
`type: 'form'` 这个经校验的「打开对象表单」写法。另外两处 —— 恰恰是 console
用户真正会看到的两处 —— 仍是退役前的旧文案:target「names no page or object to
open」,唯一出路只给 `type: 'script'`。「or object」半句描述的是已经不存在的分支,
替代能力则完全没提;最常被读到的两处给出的是最差的建议。
现在三处都从同一个构造器(`utils/modalTargetDiagnostics`)取文案。每个变体都点名
被拒 target 并指向 `type: 'form'`;console 两处额外保留 `type: 'script'` + `params`
提示 —— 它回答的是另一种作者意图(先收集输入再跑 handler),现在与 form 指引并列
而不是取而代之。`modalHandler` 的消息与 PR objectstack-ai#4764 定下的逐字节一致(这是去重,不是
改写),该字节等价性由测试钉住,各调用点确实读同一来源也由测试钉住。
三处改动前后都是硬编码英文的**编写期**诊断:它们点的是元数据键与 spec 类型,读者
是写这个 action 的人。i18n 化是另一张卡的决定;本次只让英文不再自相矛盾。
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 17, 2026
…stack-ai#4766) (objectstack-ai#4782)
`DashboardView` 自带一份独立的 `onModal` 处理器,不经 `useActionModal`,
因此仓内同时活着两份 `create_`/`new_`/`add_`/`edit_`/`update_` 前缀约定。
维护者裁定(objectstack#6739-A,2026-08-09)判该约定与 object 兜底一并退役,
PR objectstack-ai#4764 在 `useActionModal` 侧删干净了 —— 但这条路径够不到这份副本。
这份副本比 useActionModal 那份更宽:它**根本没有 page 解析**。任何字符串
target 都变成 `{ objectName }` 打开对象表单,前缀命中时还先劈成动词 + 对象。
一个真正命名 page 的 target 在这里从来就打不开。
实施前先枚举了注释自称的生产者「server-driven dashboard schemas 发出的
verb_object 名」—— 不存在。两仓语料里没有任何 dashboard 声明过
`header.actions[]`(objectstack `examples/app-{crm,showcase,todo}` 的 3 张
dashboard、`packages/apps/{account,setup,studio}`;objectui `apps/*`、
`examples/*` 含 `schema-catalog` 的 11 张 dashboard),更没有前缀形态的。
该形态在两仓仅存于 objectstack 的校验器与其钉子、以及一行文档示例。
处置不是就地改写而是**交出去**:这个视图不再自己实现契约,改为安装共享的
`useActionModal` —— RecordDetailView 与控制台运行时安装的同一个。于是
dashboard 头部按钮、记录头动作、控制台列表动作三处的解析 / 拒绝 / 报错完全
一致,契约只剩一份实现。被拒 target 的诊断即 PR objectstack-ai#4764 的那一条(点名 target
+ 指向 `type: 'form'`),因为它现在就是同一段代码。
钉子(`DashboardView.modalTarget.test.tsx`,5 条):前缀名与裸名同判、拒绝
诊断点名并指向 form、page 名正常解析并开出对话框、空 target 的独立诊断、
script 动作路径不回归。拒绝类用例都让被解析的对象**真实存在**于元数据里并
断言 object 元数据从未被问询 —— 对空对象列表断言拒绝会空洞通过。
反向验证(先预判后跑):恢复被删肢体 → 预判 4 红 1 绿,且全部以断言而非超时
转红(refusal 用例加了 `HUNG` 哨兵,把 PR objectstack-ai#4764 记录过的「打开对话框后
promise 永不 settle → 超时并污染邻居」变成快速具名失败)。实测 4 红 1 绿,
最慢 320ms。page 用例转红的是 `getItem('page', …)` 从未被调用那一条断言 ——
它的「对话框出现了」那半在变异下**仍然绿**(旧代码也开对话框,只是开错东西),
这条如实记在用例注释里。
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Retire resolveModalTarget's object fallback — a modal target names a page, only (maintainer ruling objectstack#6739-A)

2 participants

@yinlianghui@claude