Skip to content

feat(app-shell): Studio customizes an installed app's navigation into its customization package (#30059) - #30153

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-pn7lsr
Nov 4, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #30059

维护者速读

  • 改了什么:已安装(只读)包有 .custom 配对包时,Studio「界面」导轨可以编辑应用导航;首次保存把整个应用复制进配对包并声明 overrides: { package, basedOn },选择器列出配对包里的对象/页面/仪表板等,导轨显示「已定制」标记、计入「仅看定制」,并可「恢复原版」。
  • 为什么改:配对包里新建的对象此前无法进菜单,只能手敲 URL 访问。
  • 风险与回滚:仅 @object-ui/app-shell 前端;无 spec、服务端、元数据类型改动;可写包的导航编辑不变。回滚即 revert 本 PR。已定制的应用是整份副本,厂商后续版本新增的菜单项要恢复原版后重做才会出现。
  • 先看:StudioDesignSurface.tsx 的 doNavSave(首存复制进配对包)和 StudioNavItemInspector.tsx 的 gatePatch(配对包对象的菜单项自动带 requiresObject)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx -t 30059(6 条全绿)。
  • 需要你做的:评审并在合适时合并;无其他操作。

Clause-②: no
Metadata-protocol: none

What changes

On a read-only base package (installed or code-shipped) that has, or can get, its paired customization package <base>.custom (ADR-0146 D3), the Interfaces rail now edits the app's navigation through the same route a view's Customize takes. Card Scope 1–5 all ship in this PR (diff ≈ 670 lines, under the 1,500 split line).

  1. First save copies the whole app into the pair. Edit and the "+" open the existing navigation editor (addNavEntry, StudioNavItemInspector, reused). Opening Edit writes nothing. The first saved edit asks the ensure door for the pair, then writes the whole app there as a draft with overrides: { package: <base>, basedOn: <base manifest.version> } (overrideDraftBody, _ keys stripped), pinned ifNoneMatch: '*'. Later edits update that copy; the base package is never written.
  2. Pickers list the pair's items. The object picker (the pillar's object list) and the page / dashboard / report / view / doc pickers (usePackageNavTargets) read the pair after the base, the base winning on a name clash.
  3. Missing-object guard. An entry bound to an object only the pair holds carries requiresObject: <object>, so the runtime hides it if that object is removed instead of linking to "object not found". A gate this binding wrote is removed when the entry is rebound to a base object or unbound; an author's own gate is kept. (requiresObject is an existing spec key the console already evaluates; no spec change.)
  4. One batch. The navigation change is a draft in the pair's pending changes and publishes with the object it opens through the pair's existing publish-drafts (measured below).
  5. Rail mark, count, restore. When the pair serves the app (its draft or its published copy), the rail header carries the compact "customized" CustomizationMark (no text label), "only customizations" counts it and keeps the navigation listed, and the mark's popover names the base and version and offers "Restore package original", which asks for confirmation and then deletes the pair's draft and copy of the app.

A walled caller (no pair, no right to create one) is offered neither Edit nor the "+", and neither is a caller on a base whose manifest declares no version (no override can be declared then, so every save of the copy would be refused). StudioCustomizeControls.tsx changes only its types list (app added) and docblock. No spec, server, metadata-type, app_extension or upgrade-merge change.

Tests

InterfacesPillar.customize-28562.test.tsx, new describe "Customize a read-only base’s navigation into its pair (objectstack#30059)", 6 tests:

  • Edit writes nothing; the first saved edit copies the whole app into the pair with the declaration (body pinned with toStrictEqual), ifNoneMatch: '*'; the base row is untouched; the mark's facts read back.
  • The pickers offer the pair's object and dashboard; the entry bound to the pair's object is saved into the pair with requiresObject; rebound to a base object it carries no gate.
  • The mark and a count of 1 under "only customizations", which keeps the tree.
  • Restore: Cancel deletes nothing; Confirm deletes the pair's draft, then its row, and the shipped navigation reads back.
  • A walled caller sees no Edit and no "+".
  • CONTROL: on a writable package the navigation saves at the package, with no door and no declaration.

StudioCustomizeControls.customize-28562.test.tsx: app moves to the customized types.

StudioDesignSurface.oneSurface-28572.test.tsx: one existing pin changes on purpose. It said the "+" on a read-only base offers no navigation entry; it now says it does, and that creating an item still links none. navEditClosesOnReadOnly-11167 stays unchanged and green: its base declares no version, so a read-only answer still closes nav editing there.

Ablations (each through scripts/ablation-replace.mjs, restored to the pre-mutation blob):

  • Drop 'app' from STUDIO_CUSTOMIZE_TYPES → 4 failed / 2 passed (the CONTROL stays green).
  • Skip the declaration (saved = declared → saved = { ...saved }) → 2 failed / 4 passed.
  • Return no gate for a pair object (gateFor, since renamed gatePatch) → 1 failed / 5 passed.
  • The version rule has no ablation run of its own. Before it was added, the 197-file sweep showed navEditClosesOnReadOnly-11167 red on 2 tests; with the rule they pass.

Acceptance notes

Dev boot of examples/app-crm (objectstack dev --seed-admin --fresh) with the console on Vite; com.example.crm 4.0.0 is read-only (writable: false). The run was on 6f468077. The last commit changes nothing it exercised: the base has a version, and a pair-object binding writes the same gate. Screenshots are in the project's ui-orders/30059-shots/ folder; file names below.

  1. New object → menu → one batch → visible as a user. Created 合同 (crm_custom_contract) in the Data pillar: the ensure door answered 201 and created com.example.crm.custom (02-object-in-pair.png). In Interfaces, the "+" → Object added an entry; the picker listed 合同 (crm_custom_contract) after the base's objects (03-picker-lists-pair-object.png); the rail showed the mark and "saved to CRM (minimal example)'s customization" (04-entry-added-marked.png). The first save's body was the whole app (both groups, all six shipped entries, the dashboard entry's CEL visible) with overrides: {package: "com.example.crm", basedOn: "4.0.0"}. Pending changes listed object (new) + app (update) in the pair, "publishes all 2 drafts at once" (05-pending-changes-one-batch.png); POST /packages/com.example.crm.custom/publish-drafts answered publishedCount: 2, published: [object crm_custom_contract, app crm_app], one commitId. Repeated with a second object (发票) for the screenshot: same shape, publishedCount: 2, one commit. The runtime sidebar then listed 合同 → /apps/com.example.crm/crm_custom_contract (06-runtime-admin-sees-contract.png).
  2. Restore. The mark's popover reads "replaces the item of the same name in CRM (minimal example), taken from 4.0.0" (13-mark-popover-restore.png); Restore asks first (14-restore-confirm.png), then sent DELETE …?state=draft&package=com.example.crm.custom and DELETE …?package=com.example.crm.custom (both 200). The rail lost the mark and showed the shipped navigation (15-restored-shipped-nav.png), the runtime sidebar the shipped six entries (16-restored-runtime-nav.png), and GET /meta/app/crm_app?layers=true answered overlay: null, effective == code, with the base code hashing the same before and after.
  3. A member who can't read the object. An invited member (no CRM object permissions) gets 403 on /data/crm_custom_contract and /data/crm_lead. Their sidebar shows none of those entries, including 合同 (07-runtime-member-no-read.png), and the typed URL shows "no access" (08-member-opens-contract.png). Limit: this member reads none of the CRM objects, so it shows the pair's entry treated like the shipped ones, not a member who reads the base's objects but not the pair's.
  4. Removing the object. Before the gate, deleting crm_custom_contract left the runtime entry linking to "object 'crm_custom_contract' does not exist" (09-before-gate-object-removed-dead-link.png, 10-before-gate-studio-rail.png). That is why Scope item 3 above was added. With the gate written by the picker, the entry shows while the object exists (17-gated-entry-object-present.png) and is hidden after it is deleted (18-object-removed-entry-hidden.png). The Studio rail still lists it, so the author can remove it.

Observed, not changed: the pending-changes row and the Restore dialog name the app by its raw label "CRM" while the rail shows the translated "客户管理".

Verification

All under scripts/pm/os-verify-lock.sh unless noted. Head 6e106af3 (main merged after #30113 landed).

  • Build: turbo run build for @object-ui/app-shell..., 119/119 tasks.
  • @object-ui/app-shell type-check (both tsconfigs) → exit 0, before and after merging main.
  • Tests: every app-shell test file importing StudioDesignSurface, StudioNavItemInspector, InterfacesNavRail or StudioCustomizeControls: 197 files, 1359 tests, all passed. The first sweep found 3 red files, fixed in bee40cbe (see Tests). After the merge, the 8 files the merge and this PR share: 180 passed.
  • npx eslint on the 6 touched source/test files (unlocked): 0 errors, 58 warnings.
  • node scripts/check-sdui-manifest.mjs → exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 35 commands, all recorded :: exit 0; --ran → "35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN". One run was red at first: pnpm --dir ui exec vitest run scripts/ had 2 tests time out at 15 s while the 197-file sweep shared the box. Those 2 files then passed alone, and the whole command passed on rerun (170 files, exit 0). That rerun's exit is the recorded one.
  • Changed lines vs the human-merge threshold: 677 (+593 / −84, 8 files), under 3,000.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzerTiGyoUPgrA6K1SY

…30059)

On a read-only base with a customization pair, the Interfaces rail's
navigation editor now edits the app: the first saved edit copies the whole
app into the pair, declaring the override at the base's version, the same
route a view's Customize takes. The target pickers list the pair's items, the
rail marks the served override and counts it under "only customizations",
and Restore package original deletes it after a confirmation.

Claude-Session: https://claude.ai/code/session_01WGufzerTiGyoUPgrA6K1SY
Co-authored-by: Claude <noreply@anthropic.com>
…touched nav pins (#30059)

Measured on a dev boot of examples/app-crm: with an object made in
com.example.crm.custom on the customized navigation, removing the object
left the runtime menu linking to "object not found". An entry the
inspector binds to an object only the pair holds now carries
`requiresObject: <name>`, so the runtime hides it once the object is
gone (measured: hidden after removal, shown while it exists). A gate the
binding wrote is removed when the entry is rebound to an object the base
ships or unbound; an author's own gate is kept.

Then, from the full sweep of the app-shell test files that import the edited components:

- sharedSelect-11865 pins the keys an object binding writes; the gate
  added `requiresObject: undefined` to every binding. The binding now
  writes the key only when it sets a gate or removes one it wrote.
- navEditClosesOnReadOnly-11167 pins that a read-only answer closes nav
  editing. Its base declares no manifest version, so no override can be
  declared and every save of the copy would be refused: the nav editor
  is now offered on a read-only base only when the base has a version.
- oneSurface-28572 pinned that the "+" on a read-only base offers no nav
  entry, which #30059 changes on purpose; the pin now says it does, and
  that creating an item still links none.

Claude-Session: https://claude.ai/code/session_01WGufzerTiGyoUPgrA6K1SY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 4, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd

维护者速读(席位终稿)

  • 改了什么:Studio「界面」导航栏现在能定制已安装(只读)应用的导航。在只读包上点「编辑」或「+」,用的是可写包同一个导航编辑器;第一次保存时把整个应用复制进配对的 .custom 定制包,声明为覆盖(overrides: { package, basedOn }),基础包不动。挑选器会列出定制包里的对象、页面、仪表盘和报表。导航改动作为待发布变更,和它打开的对象一次发布。导航栏有「已定制」标记,「仅看定制」会计数,标记里可以「恢复原版」(带确认)。
  • 为什么改:原来在只读包上新建的对象没法放进菜单,用户只能手打网址才能进去。
  • 风险与回滚:中低。只动 Studio 的界面栏和导航编辑器,应用端、元数据形状和服务端都不变,没有新键或新类型。可写包的导航编辑不变(有对照测试)。代价按卡约定:覆盖是整份复制,厂商后续版本新增的菜单项要恢复原版后才出现。回滚即 revert 本 PR。
  • 先看:StudioDesignSurface.tsx 里首次保存走 overrideDraftBody 的那段,以及 StudioNavItemInspector.tsx 的挑选器(定制包条目、requiresObject 门)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx。
  • 需要你做的:无。这不是受管面,CI 全绿、运行时截图补拍后席位负责落地。
  • 席位意见:ACCEPT。

席位核对 · ACCEPT · head 6e106af3437be591f947a0ffd323adb05dd2fdcc · 2026-11-04T16:51Z

  1. Shape:
  2. Fix, read by the seat (Scope 1–5):
    • Door: app is added to STUDIO_CUSTOMIZE_TYPES. Nav customizing is offered only when the ensure door does not wall the caller and the base has a version (basedOn), so every save can carry a declaration.
    • First save: the whole app is written into the pair through overrideDraftBody(saved, custom.base, custom.basedOn). That is the same route a view's 定制 takes. Opening Edit writes nothing, and the base package is never written.
    • Editor reuse: addNavEntry and StudioNavItemInspector. There is no second editor.
    • Pickers: the pair's items are listed after the base's. An entry bound to an object that only the pair holds carries requiresObject, a gate the browser run asked for (Acceptance 4).
    • Mark, count and Restore: the rail mark (no text label), the 仅看定制 count, and 恢复原版 behind a confirmation.
    • One existing pin (oneSurface-28572: the read-only "+" offers no nav entry) was updated on purpose, because that behaviour is what this card changes.
  3. Evidence (from the dev report, 6636489980):
    • 6 new tests, including the CONTROL (a writable package saves at the package with no declaration).
    • Three ablations each turn their pin red while the CONTROL stays green: drop app, skip the declaration, drop the gate.
    • Sweep of the 197 app-shell files that import the touched modules: green. Type-check exits 0. 35/35 derived gates were run (--ran: 0 NOT-MEASURED).
    • Browser on app-crm (com.example.crm 4.0.0, read-only):
      • Acceptance 1: the same-batch publish was measured at publishedCount 2 (object + app) with one commitId.
      • Acceptance 2: Restore leaves overlay null and effective equal to code, with the base hash unchanged.
      • Acceptance 3: a member without read access gets 403, and the entry is absent from their sidebar.
      • Acceptance 4: with the gate, the entry is hidden once its object is deleted.
    • The seat read 04-entry-added-marked, which is fine. The runtime frames (06, 16, 17, 18) are covered by the 设置默认货币 dialog, so the sidebar cannot be read in them. The dev is retaking them into /mnt/project-files/ui-orders/30059-shots/, with no code change.
  4. Noted, not a blocker: the pending-changes row and the Restore dialog show the raw app label CRM, while the rail shows the translated 客户管理. This is noted for a possible follow-up.
  5. Landing: ordinary queue. It needs the retaken runtime shots, plus every check on this head green or an expected skip. Then the seat relays pr_ready + automerge_enable, follows it to MERGED, and records the landing on Studio: customize an installed app's navigation through the existing app override, so a new object can get a menu entry #30059.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 17:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 6cc0b81 Nov 4, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-pn7lsr branch November 4, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: customize an installed app's navigation through the existing app override, so a new object can get a menu entry

1 participant