Skip to content

fix(console): a record page leaves out related lists and rail entries of an object the install does not hold - #27180

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ml8oyu
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ml8oyu

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #26988

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:记录页上,绑定到当前安装里不存在的对象的相关列表和右栏条目不再绘制,也不再为它发 /meta/object、/data 请求。
  • 为什么改:ADR-0141 之后模块对象可以真实缺席,页面却照画「Crm Quote 0 — No related records found」这类拿原始名拼出的假标题,并打出一串 404。
  • 风险与回滚:只在已注册对象列表非空时裁剪,与仪表盘 requiresObject 同一规则;回滚即撤掉 RecordDetailView.tsx 里两处 pruneUnregisteredRecordPageObjects 调用。
  • 先看:recordPageChildObjects.ts(裁剪函数)、RecordDetailView.tsx(assignedPage / assignedSlots 两个 memo)、新测试文件。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.unregisteredObjectLists-26988.test.tsx,预期 2 个用例通过。
  • 需要你做的:无,席位落地。

What changes

  • recordPageChildObjects.ts gains pruneUnregisteredRecordPageObjects(page, registered). It reads the same two arms as recordPageChildObjects (now shared helpers): a record:related_list block whose properties.objectName or dataSource.object names an object the registered list does not hold is removed; a record:reference_rail entry whose objectName does is removed, and a rail left with no entries goes with it. Only a string names an object. While the registered list is empty it returns the page untouched (the deferral DashboardView makes), and it is identity-preserving: a page or subtree with nothing to leave out is returned itself.
  • RecordDetailView prunes the authored page and a slotted page's slots right where assignedPage / assignedSlots are resolved, so the effective page, the synthesized page, the composed page and the history page all read one pruned page.
  • The registered list is the view's own objects prop, the list deriveRelatedLists already reads. It is not useMetadata().objects: that getter loads the whole registry, which the scoped record route (objectui#13046) exists to avoid. On that route objects is the scope: the package's objects plus every object the page binds, each asked by name.
  • The child-write subscription from Record page: after creating a child from a related list, the parent's header keeps the pre-create rollup (Total Price 32,400 while the server holds 37,800) until a reload #26890 reads the pruned page. An object the install does not hold cannot be written, so there is nothing to watch on it.
  • Derived related lists (deriveRelatedLists) were already safe: they come from the registered objects only.

Tests

  • RecordDetailView.unregisteredObjectLists-26988.test.tsx uses the real page. It has a related list bound by objectName, one bound by dataSource.object, and a rail entry, all three for absent objects, beside a registered contact list and rail entry.
    • Absent arm: no humanized name ("Crm Quote" and the others), no rows, and no data-source call or fetch URL names any of the three objects. The contact list and rail entry still draw and read.
    • Present arm: the same page draws all three and reads every object.
  • recordPageChildObjects.test.ts: pins what is left out and what is kept, the deferral (an empty set prunes nothing), identity preservation, that the authored page is never mutated, and that non-string names are kept.
  • Ablation: scripts/ablation-replace.mjs replaced the guard line if (registered.size === 0) return page; with an unconditional return page;. The absent-arm test went red (expected '…' not to contain 'Crm Quote') and the present arm stayed green. The tool restored the file (blob == HEAD, git diff HEAD empty).

Acceptance notes

  • On a scoped record route (a full declared record page in a packaged app), the scope still asks /meta/object/NAME once for each name the page binds, absent ones included. That read is how the console learns the object is absent; it is the one existence check, and this PR adds none. Everything the page itself asked (each block's schema read and /data read) is gone.
  • A tab or container whose only content was a pruned list stays, now empty. Pruning containers is not part of this card.
  • record:line_items (childObject) is not pruned. It is outside this card's two arms.

Verification

Gate readings and the head they were taken on are in the os-dev report comment on #26988.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (Local: eslint on the four changed files, 0 errors; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Nothing publishes here, so no changeset.)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

@object-ui/app-shell is private: true and nothing else changed, so this PR takes skip-changeset.


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · head 7fa6ed4e · 2026-10-28T09:42Z

席位意见:记录页上作者放的相关列表块或引用栏条目,如果绑定的对象在本安装里不存在(模块未装,ADR-0141),原先照样画出来,显示人性化的原始名("Crm Quote 0 — No related records found")并请求 404。现在按 DashboardView 同一规则(已注册对象列表)把它们剪掉,列表未加载前不剪。

  1. Shape: Fixes #26988; 4 files, +516/−9: RecordDetailView.tsx +23/−2 (the prune beside assignedPage / assignedSlots, inside the claimed region), recordPageChildObjects.ts +84/−6 (the pure pruneUnregisteredRecordPageObjects sharing Record page: after creating a child from a related list, the parent's header keeps the pre-create rollup (Total Price 32,400 while the server holds 37,800) until a reload #26890's two arms), and tests (helper 6, page-level 2). skip-changeset: @object-ui/app-shell is private: true.
  2. Premise measured: the order's mechanism held. The seat read the diff and measured the one deviation: the registered list is the view's own objects prop (the list deriveRelatedLists already reads), not useMetadata().objects, which would load the whole registry on a scoped record route (objectui#12811 / docs(spec): name the real policy-gate runtime symbols in the publish-gate consumer table #13046). On a scoped record route that list is readPageObjectScope over the declared record page, and pageObjectNames collects every objectName and dataSource.object at any depth, so a related list's and a rail entry's object is asked and waited for before the route draws; an installed one is never pruned there. A record overlay (hasRecordOverlay) reads the whole registry. usePageAssignment takes page and slots from the same matched page, so the slots ride the same scope. The prune is identity-preserving and deferred on an empty list.
  3. Gates / check-runs: PR check-runs on 7fa6ed4e: 43 success, 16 skipped (path-filtered), 0 failure; Lint & Repo Gates, TypeScript Type Check, Type Check UI and Test UI 16/16 success; mergeable_state clean. Dev: 24 derived, 24 run, 0 NOT-MEASURED (one scripts/ 15 s timeout unlocked, rerun alone 83/83); 23 rerun on the merged head, all exit 0; 77 RecordDetailView files / 511 tests green; app-shell type-check exit 0. Ablation (the prune made a no-op): the absent arm turns red ("Crm Quote" drawn), restored byte-identical.
  4. Accepted deviations: the registered list source (item 2). On a scoped record route the scope's own by-name read of an absent object still answers 404 once: that read is the existence check the prune reads, not a second one, and the page itself then asks nothing for it. A tab emptied by the prune stays as an empty tab.
  5. Out of scope: an empty tab left by the prune is not hidden: a finding for a later card if it reproduces on a real app. Skills-docs: none — record page render only.
  6. Landing: not Tier H (no governed path, +516 lines); ready for landing (ready + auto-merge).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants