Skip to content

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

Closed

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

The maintainer: 「按「覆盖」立卡」 (2026-11-04), after 「其他元数据是怎么处理的。很多是叠加覆盖对吧。」 The governing ruling is ADR-0146 D2: 「方向定为包级覆盖:代码和 Studio 用同一机制,定制都进 .custom。」 Measured by the seat on main 63b86b385d, on the example CRM. Code re-read on c23501b2b9. No domain:* label is set: triage routes this card.

Business reason

An operations person adds a contract object to an installed CRM and publishes it. The object works, but users can reach it only by a typed URL, because Studio offers no way to put it on the app's menu. This is the normal case: real apps are installed, so they are read-only. The customization is finished in every way except the one step that makes it usable.

What already works, with no code change

An override of the app in the paired customization package, written through the existing metadata door:

  • Item: app named crm_app, in com.example.crm.custom, with overrides: { package: 'com.example.crm', basedOn: '4.0.0' }, a whole copy of the app, and one extra entry under group_sales: { id: 'nav_eval_hetong', type: 'object', objectName: 'crm_custom_eval_1104_hetong', label: '合同' }.
Step Measured result
Save as draft, then POST /api/v1/packages/com.example.crm.custom/publish-drafts published, 1 item
GET /api/v1/meta/app/crm_app The entry is served under group_sales, with _packageId still com.example.crm
Running app The sidebar shows the entry and opens the list
Studio Interfaces rail 合同 appears under 销售管理
Restart the backend Still there
DELETE /api/v1/meta/app/crm_app?package=com.example.crm.custom The entry leaves, and the app reads as shipped

GET /api/v1/packages/com.example.crm/customizations lists the override.

What is missing: the Studio door

  • ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx:73: STUDIO_CUSTOMIZE_TYPES is view, dashboard and report. app is overridable (packages/spec/src/kernel/customization-package.ts:325) but Studio does not offer it.
  • ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:4651 on c23501b2b9: on a read-only base the rail's 「+」 offers only what it can create (page, dashboard, report), and the 编辑 navigation button draws only when the package is writable.
  • On the measured override: the 合同 row carries no mark, 仅看定制 did not count the app override, and there is no way back from the rail.

Scope

  1. On a read-only base with a pair, the navigation can be customized. The first save takes a whole copy of the app into the pair with the overrides declaration, as a view's 定制 already does. basedOn is the base's manifest.version.
  2. It reuses the writable package's navigation editor (addNavEntry, StudioNavItemInspector): add object, page, dashboard, report and the other entry kinds, plus rename, reorder, group and remove, including entries the base shipped.
  3. The target pickers offer the pair's own items as well as the base's. A person can add the object they just made in the pair, and an already existing page, dashboard or report listed under 不在导航中.
  4. A navigation change is a draft in 待发布的变更 and publishes in one batch with the object it opens.
  5. The rail shows that the navigation is customized, 仅看定制 counts it, and 恢复原版 removes the override after a confirmation.

Acceptance

  1. The job end to end in a browser: new object in the pair, add it to the menu, publish both together, see it as a user. Publishing the object and the app in the same batch was not measured here, so measure it.
  2. 恢复原版 returns the shipped navigation. The base package is unchanged throughout.
  3. A member who cannot read the object does not get a working entry. This was not measured. Say on the PR what the member sees.
  4. An entry whose object is later removed does not leave a broken link. Use the existing requiresObject if that is what the measurement calls for.
  5. No new metadata type and no spec change. If the server turns out to need a change, report it on the card with the reason.
  6. Tests pin the first-save copy, the pair target pickers and the rail mark and count. Screenshots on the PR, from latest main.

The accepted cost

An override is a whole copy. A navigation entry the vendor adds in a later version does not appear in a customized app until the override is restored and redone. ADR-0146 D2 records basedOn and does not compare it, and upgrade comparison is out of that epic. This cost follows from the rule and was not measured with a real upgrade. The maintainer chose this over an additive new metadata type, which would have kept vendor entries but added a second rule and server work. Do not add a comparison or a merge in this card.

Not in this card

Re-check

git grep -n "STUDIO_CUSTOMIZE_TYPES: readonly" origin/main -- ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx

Activity

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p1 kept · domain:ui · 2026-11-04T13:00Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 4, 2026
  3. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

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

    Claim: PM loop round 1 · 2026-11-04T14:49Z
    Session: session_01WGufzerTiGyoUPgrA6K1SY (card thread; dispatching seat session_01WGufzjDyeFwq6AuFu3vwTd)
    Account: os-support-ai
    Branch: claude/project-thread-pn7lsr (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-30059
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx (types list + app door only), the Interfaces rail / navigation editor in StudioDesignSurface.tsx, InterfacesNavRail.tsx, StudioNavItemInspector.tsx; their tests, browser screenshots under /mnt/project-files/ui-orders/30059-shots/, one root .changeset/ file; ⛔ no spec change, no new metadata type, no upgrade comparison/merge, no app_extension (stop on breach; explain in the report)
    Container & model: L (Scope 1–4 first as Refs if the diff passes ~1,500 lines), mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: ui code produces it (domain:ui) | no platform path covers it | every Studio author customizing an installed app
    Thread-read: 6631291035
    Premise: holds (re-read on origin/main f68b47cac: STUDIO_CUSTOMIZE_TYPES is ['view','dashboard','report']; the rail's 「+」 on a read-only base offers only creatable kinds)
    Serial constraints cleared: #30057 (same seat, PR #30113 ACCEPTed) touches ObjectFormDesigner.tsx and the Data pillar in StudioDesignSurface.tsx; this card keeps to the Interfaces rail and merges main before its PR if #30113 lands first. #30046 unclaimed.
    Order: /mnt/project-files/ui-orders/30059.md · maintainer scope 「总并发上线 10」 (2026-11-04T14:01Z)

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 4, 2026
  5. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-support-ai · session_01WGufzerTiGyoUPgrA6K1SY
    os-dev-report
    Stamp: 2026-11-04T16:48Z

    {
      "issue": 30059,
      "status": "done",
      "branch": "claude/project-thread-pn7lsr",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30153",
      "session": "session_01WGufzerTiGyoUPgrA6K1SY",
      "premise_still_valid": true,
      "summary": "Draft PR #30153, Fixes #30059, head 6e106af3437be591f947a0ffd323adb05dd2fdcc, no assignee and no reviewer. Scope 1 to 5 ship in one PR: 677 changed lines, under the 1,500 split line. On a read-only base with a pair (or one the ensure door can create), the Interfaces rail's Edit and its \"+\" open the existing nav editor (addNavEntry and StudioNavItemInspector, reused). Opening Edit writes nothing. The first saved edit copies the whole app into the pair as a draft with overrides {package, basedOn: base manifest.version}. The pickers list the pair's items after the base's. The rail header carries the compact customized mark (no text label); only-customizations counts it; the mark's popover offers Restore package original behind a confirmation. Added from the browser run: an entry bound to an object only the pair holds carries requiresObject, because deleting such an object otherwise left a dead runtime link. Nav customizing is also offered only when the base manifest has a version, and one existing pin (oneSurface-28572: the \"+\" on a read-only base offers no nav entry) was updated on purpose. No spec, server or metadata-type change. StudioCustomizeControls.tsx changes only its types list and docblock. #30113 (#30057) landed first and main is merged; it merged without conflicts.",
      "tests": "New describe in InterfacesPillar.customize-28562.test.tsx, 6 tests: the first-save whole copy with the declaration (body toStrictEqual, ifNoneMatch *, base untouched); the pickers offer the pair's object and dashboard, the pair-object entry is saved with requiresObject, and a rebind to a base object leaves no gate; the mark plus a count of 1 under only-customizations; Restore cancel and confirm; walled caller sees no Edit and no \"+\"; CONTROL: a writable package saves at the package with no door and no declaration. Ablations through scripts/ablation-replace.mjs, each restored to the pre-mutation blob: drop 'app' from STUDIO_CUSTOMIZE_TYPES gave 4 failed and 2 passed (CONTROL green); skip the declaration (saved = declared to saved = { ...saved }) gave 2 failed and 4 passed; no gate for a pair object gave 1 failed and 5 passed. Sweep of every app-shell test file importing StudioDesignSurface, StudioNavItemInspector, InterfacesNavRail or StudioCustomizeControls under the lock: the first pass had 197 files with 3 red (sharedSelect-11865 keys, navEditClosesOnReadOnly-11167, oneSurface-28572), all fixed in bee40cbe; the rerun gave Test Files 197 passed and Tests 1359 passed, VERDICT command-exit 0. After merging main, the 8 shared files gave 180 passed. app-shell type-check exited 0 before and after the merge. eslint on the touched files: 0 errors. Browser on a dev boot of examples/app-crm (com.example.crm 4.0.0 read-only), 16 screenshots in /mnt/project-files/ui-orders/30059-shots/. Acceptance 1: the ensure door answered 201, the object draft was created in the pair, the picker listed it, and publish-drafts on the pair answered publishedCount 2 (object plus app) with one commitId, measured twice; the runtime sidebar showed the entry. Acceptance 2: Restore sent DELETE of the draft and the row at the pair; layers=true read overlay null and effective == code, with the base code hash unchanged; the runtime showed the shipped nav. Acceptance 3: an invited member got 403 on the object; the entry was absent from their sidebar and the typed URL showed no access. Acceptance 4: deleting the object left a dead link before the gate; with the gate the entry showed while the object existed and was hidden after deletion. The browser run was on 6f468077; the last commit changes nothing it exercised.",
      "gates": "dispatch-gates.mjs --commands after fetching origin/main: 35 commands, run from a runner script, each exit recorded. 34 exited 0 the first time. pnpm --dir ui exec vitest run scripts/ exited 1 with 2 tests timing out at 15 s while the 197-file sweep shared the box; both files passed alone under the lock, and the whole command reran to exit 0 (170 files). The recorded line is that rerun. --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The gate sweep ran before the last app-shell fixes and the merge of main. Those changes are app-shell source and tests only, and they were re-tested above. check-sdui-manifest exited 0. CI check-runs on #30153 have not been read yet.",
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm.",
      "mcp_calls": "0 MCP GitHub calls; reads went through gh api REST. Other MCP: claude-code-remote subscribe_pr_activity on #30153 (not a GitHub write).",
      "api_writes": "4: git push of 284ece45 (first slice); git push of 6e106af3 (follow-up commit plus a merge of main); pr_create through the relay (fleet-write run 41870856456, #30153, read-back identical at 10678 bytes, assignees none); this comment via post-stamped.mjs. One push was refused locally by check:commit-card-trailers (Refs #30059 in two unpublished messages, reworded before pushing), so nothing was sent on that one.",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · noted, not filed · The pending-changes row and the Restore dialog name the app by its raw label CRM, while the Interfaces rail shows the translated label 客户管理 (zh-CN, app-crm dev boot). Noted in the PR's Acceptance notes."
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

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

    Landed · PR #30153 · 6cc0b818c2 · 2026-11-04T17:53Z

    Probe Reading
    PR #30153 merged by the merge queue at 2026-11-04T17:52Z, merge commit 6cc0b818c2
    Head at landing 6e106af3, CI green (44 success, 11 expected skips)

    The seat's ACCEPT is comment 6636579502. The card is closed by Fixes #30059 (state: closed completed). pm:dispatched is removed in this stroke.

    • On a read-only base with a .custom pair, Studio's Interfaces rail now edits the app's navigation with the writable package's editor.
      • The first saved edit copies the whole app into the pair, with overrides: { package, basedOn }.
      • The pickers list the pair's items.
      • An entry for a pair-only object carries requiresObject.
      • The navigation draft publishes in one batch with the object it opens. This was measured at publishedCount 2, one commit.
      • The rail shows a mark, 仅看定制 counts it, and the mark offers 恢复原版.
    • The runtime shots were retaken without the currency dialog: /mnt/project-files/ui-orders/30059-shots/ (06–10, 15–18, plus 09b for the dead link before the gate).
    • Not filed, and filed only if the maintainer asks: the pending-changes row and the Restore dialog show the raw app label CRM, while the rail shows 客户管理.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions