Skip to content

feat(studio): customize a read-only package's view, dashboard or report into its <base>.custom pair, with an override badge and "Restore package original" - #28711

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-uv3zpb
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-uv3zpb

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #28562

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 打开只读基础包(如 HotCRM)的视图、仪表板、报表,或对象条目的列表视图时,标题栏多一个「自定义」。点它只解锁编辑器,不写任何东西。第一次保存编辑时,每个条目调用一次 ensure 门(POST /packages/:id/ensure-customization)。之后把草稿写进门回答的配对包 <base>.custom,草稿声明 overrides: { package, basedOn },basedOn 取包列表里基础包的 manifest.version。待发布数、变更面板和发布都改为作用于配对包。标题栏显示「覆盖 HotCRM」徽标,读自服务器返回的声明。⋯ 菜单新增「恢复软件包原版」:确认后先删草稿,再删配对包里的这一行,基础包的版本随即读回。只读标记不再把这三种类型链接到 Setup。
  • 为什么改:已安装包在 Studio 里只读,管理员想改 HotCRM 的列表列、仪表板或报表,又不想动 HotCRM 本身(ADR-0146 D2/D3)。
  • 风险与回滚:纯前端改动,只调用已上线的门:ensure、草稿保存、批量发布和删除。可写包的行为不变(有对照测试)。纯 revert 即可回滚。
  • 先看:StudioDesignSurface.tsx 里 InterfacesPillar 的 doSave(配对包 id 取自门的回答)、restoreOriginal,以及 Studio 外壳里的 draftScope / publishTarget;新文件 StudioCustomizeControls.tsx。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx packages/app-shell/src/views/studio-design/StudioDesignSurface.customizePublish-28562.test.tsx,预期 19 例全绿。
  • 需要你做的:无,由席位落地。

What changes

  • Customize (StudioCustomizeControls.tsx, new) appears in the item header of a view, dashboard or report, and of an object leaf's list view, when the package is read-only. Pressing it only unlocks that item's editors (canvas, inspectors, list-view panel). Nothing is written until an edit is saved, so an unedited Customize leaves nothing behind.
  • First saved edit (InterfacesPillar doSave, and the list-view hook's save via its new customization option). It calls ensureCustomizationPackage (packages-io.ts, new) once per item, with no prompt and no body. It then saves the draft into the package the door's answer names. The body is the item as held, without its top-level _ keys, with overrides: { package: <base>, basedOn: <listed manifest.version> }. The answer is judged by the spec's EnsureCustomizationPackageResponseSchema. A declaration the item already carries is kept as it is. The first save into the pair is pinned If-None-Match: * when the buffer was read at the base, so a draft someone else left there is asked about, never overwritten.
  • Walled. Under the group or isolated posture, an admin who is not the platform operator sees Customize disabled, with the reason. This is read from features.tenancyPosture and user.isPlatformAdmin, the facts useFlowLedgerWalledReadOnly reads. Any refusal from the door is shown as the envelope reader writes it (<sentence> (<CODE>)), and nothing is saved.
  • No version, no guess. A base whose listed manifest states no version gets Customize disabled, with a sentence that says so.
  • Pending, panel, Publish (Studio surface). On a read-only base with a pair (the door's answer, else the pair the package list lists), the pending feed, DraftChangesPanel and publishPackageDrafts name the pair. A refusal at the publish door goes through the existing refusal path, verbatim. A read-only base with no pair keeps the read-only Publish and its reason.
  • Badge (StudioOverrideChip). It reads the declaration the server serves: the pair's draft through the draft door, else the layered read's overlay (N-10). Its title names the version. It is never computed.
  • Restore package original (StudioRestoreOriginalItem + StudioRestoreOriginalDialog) is on the ⋯ menu of an item served as an override. On confirm it sends DELETE /meta/:type/:name?state=draft&package=<pair>, then DELETE /meta/:type/:name?package=<pair>, and reloads the item. The base reads back. The pair is the door's answer this item's saves named, else the surface's. The door is not asked again.
  • StudioReadOnlyLink no longer links a view, dashboard or report to Setup's overlay page. Other overlay-able types keep the link.
  • Strings. Twelve engine.studio.customize.* keys in the studio namespace (en + zh). The placeholder is {package}, because the zh census refuses a bare Latin base.

Surface additions (approved by the seat)

  • ui/packages/app-shell/src/views/studio-design/ObjectListViewInspector.tsx. useObjectListViewDraft gains an optional customization (readAt / writeTarget), and ObjectListViewInspector gains canCreate (default !readOnly). Without them nothing changes.
  • ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx (new).

Tests

  • StudioCustomizeControls.customize-28562.test.tsx (36): type gate, declaration parse, override body (_ keys dropped, an existing declaration kept, null with no version), the wall matrix, the ensure client (201 and 200, no body, refusal text, a malformed answer refused, packages-changed only on create), listed version and pair, and the button and badge.
  • InterfacesPillar.customize-28562.test.tsx (13). The mounted pillar, real autosaves, and the real ensure client over a fetch stub that answers 201, then 200, are driven against a server double. Covered:
    • Customize writes nothing.
    • One ensure call per first edit, with the draft at the pair carrying the declaration and the base untouched.
    • A second item answered 200.
    • Pins: If-None-Match: *, then If-Match.
    • A refused door shown verbatim.
    • Walled disabled, and no version disabled.
    • No Setup link.
    • Restore on a dashboard (two deletes naming the pair, base label back) and Cancel.
    • The list view customized into the pair ("Add view" stays off).
    • Restore bringing back the base's columns.
    • CONTROL: a writable package saves in place with no door and no declaration.
  • StudioDesignSurface.customizePublish-28562.test.tsx (6). The real surface and the real client over fetch. Covered:
    • Pending reads _drafts?packageId=<pair>, the panel reviews the pair, and Publish posts /packages/<pair>/publish-drafts.
    • End to end, from first edit to door to pair draft at the listed 4.0.0, then Pending and Publish follow the pair.
    • No version disabled.
    • A group posture plus a non-operator is walled, and the platform operator is not.
    • CONTROL: no pair keeps the read-only Publish on the base.
  • StudioReadOnlyLink.overlay-27356.test.tsx was updated. View, dashboard and report are plain. An overlay-able email_template still links (control).

Ablations

Nineteen, each through scripts/ablation-replace.mjs. Each one landed, its filtered test went red, and the file was restored to HEAD:

# Mutation Red
A1 pairFor never reuses an answer one ensure per first edit
A2 save sends the body without the declaration first saved edit
A3 save writes at the base, not the pair first saved edit
A4 ensure keyed per base, not per item second item gets 200
A5 customizeDoorWalled always false walled admin (surface)
A19 pillar ignores walled walled caller (pillar)
A6 publishTarget = packageId Publish targets the pair
A7 draftScope = packageId Pending/panel on the pair
A8 publishReadOnly = readOnly Publish enabled on the pair
A9 Restore skips the row delete Restore brings back the base's columns
A10 served overlay declaration ignored badge on a served override
A11 no noVersion block no version disabled
A12 StudioReadOnlyLink ignores the three types no Setup link (3 cases)
A13 canCreate not passed "Add view" stays off
A14 no If-None-Match pin (leaf) pins
A15 no If-None-Match pin (list view) list view customize
A16 leafReadOnly always false Customize writes nothing
A17 basedOn not read from the listed manifest end to end at 4.0.0
A18 save uses the listed pair, not the door's answer first saved edit

Live measure

This was measured on pnpm dev:crm (base com.example.crm, read-only, view crm_lead.all) through the server's doors with curl. The browser UI was not driven; the UI is covered by the tests above.

  • POST /api/v1/packages/com.example.crm/ensure-customization answered 201 with created: true on the first call and 200 with created: false after. data.packageId was com.example.crm.custom, and the UI takes the pair from that answer.
  • GET /api/v1/packages served com.example.crm with manifest.version 4.0.0 and writable: false.
  • PUT /meta/view/crm_lead.all?mode=draft&package=com.example.crm.custom (If-None-Match: *) answered 200. The draft read at the pair carried overrides: { package: "com.example.crm", basedOn: "4.0.0" }, and _drafts?packageId=com.example.crm.custom listed it.
  • POST /packages/com.example.crm.custom/publish-drafts answered published, 1. Then GET …/layers?package=com.example.crm returned overlay.overrides = the declaration and effective.label "All leads (customized)".
  • Restore: DELETE …?state=draft&package=com.example.crm.custom answered 200, then DELETE …?package=com.example.crm.custom answered 200. The layered read then returned overlay null and effective.label "All Leads", the base read back.

Acceptance notes

  • Not here (per the card): rail marks, filter, switcher fold, New (N-19), flows (N-18), translations and email templates (N-25), the rest of the link and the package-less write (N-16b), and migration (N-16a).
  • Deviations:
    • The item tools (history, source) stay read-only on a read-only base, also while customizing.
    • "Add view" stays off on a read-only base. A new view would be a new item of the base, which is N-19.
    • A list view the server does not hold yet, such as an object's default view that was never created, offers no Customize.
    • A walled admin's Restore is not disabled ahead of time. The server's verdict is shown.
    • The If-None-Match: * pin on the first pair save is an addition.

Verification

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jQpUMdR7QmHKTFR8iB

The zh wording census reads a bare Latin "base" in a value as an untranslated
word, and {base} is one.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jQpUMdR7QmHKTFR8iB
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tooling labels Nov 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

席位核对 · ACCEPT · 2026-11-01T15:00Z

Head-sha: 1ae8358eb790d3b6f482ccd3590f57bc96b001a2

  • Shape: draft → main, first content line Fixes #28562, no other card number beside a closing keyword. 11 files, +2183 / −51 (2234 lines ≤ 3000), no governed path (check-governed-merges --pr 28711: 0 of 11 paths hit the register). Author objectstack-fleet[bot]. Clause-②: no / Metadata-protocol: none line-initial in body and changeset, as claimed. The 12 new keys sit in @object-ui/i18n's studio namespace (Record<string, string>, en + zh); no locales/* pack key moved; no spec file is touched. No contract review is owed.
  • File surface: inside the claim, plus the two additions the seat accepted before pr_create: ObjectListViewInspector.tsx (the list view's read/write address) and the new StudioCustomizeControls.tsx. packages-io.ts gains the ensure door and two list readers.
  • Wanted, item by item, read in the diff:
    1. Customize (StudioCustomizeButton): drawn in the header of a view, dashboard or report and of an object leaf's list view, on a read-only base only. It is disabled under a wall for a non-operator (postureHasOrgWall + user.isPlatformAdmin, the facts useFlowLedgerWalledReadOnly reads), with the reason as the wrapper's title and description. A door refusal is plainRefusal of the server's sentence.
    2. First saved edit: pairFor asks ensureCustomizationPackage once per item (again only after a refusal). The draft goes to the answer's packageId under the same type and name. Its body is the document without top-level _ keys, declaring overrides: { package, basedOn } with basedOn = the listed manifest.version (overrideDraftBody). An existing declaration is kept, and a base with no version saves nothing and says so. The answer is judged by EnsureCustomizationPackageResponseSchema. Pressing Customize writes nothing.
    3. Publish: the pending count, the changes panel and Publish address draftScope, the pair once there is one (publishReadOnly only while no pair exists). N-09's refusal stays the existing verbatim path.
    4. Badge (StudioOverrideChip): drawn only from useServedOverride, which reads the pair's draft, else the layered read's overlay, through OverrideDeclarationSchema. Nothing is computed.
    5. Restore package original: on the item's ⋯ menu, with the confirmation held outside the menu. It sends reset state: 'draft' then the row, both at the pair, and reloads the buffer and the served declaration.
    6. Link: StudioReadOnlyLink no longer offers the overlay link for isStudioCustomizeType types.
  • Live measure (dev, pnpm dev:crm, curl through the doors):
    • ensure answered 201 created: true, then 200 created: false, with data.packageId com.example.crm.custom;
    • the pair draft carried overrides: { package: "com.example.crm", basedOn: "4.0.0" };
    • after publish-drafts, the layered read served overlay.overrides and the customized label;
    • Restore's two deletes answered 200, and the base's label read back with overlay null.
    • The browser was not driven; the UI is covered by the tests.
  • Gates (check-runs on this head): 44 success, 11 skipped, 0 failed or pending. All ten required contexts are included; Build Core is skipped by its path filter on a ui-only diff. main gained e170d424 (feat(spec)!: judge a collection item's _packageId against its owning body at defineStack, os validate and os build #28685, spec-only) after the dev's last merge. It merges clean and touches none of these files. The merge queue builds on it.
  • Tests: three new suites (InterfacesPillar.customize-28562, StudioCustomizeControls.customize-28562, StudioDesignSurface.customizePublish-28562) and StudioReadOnlyLink.overlay-27356 updated. Nineteen ablations (A1–A19 in the body), each red then restored.
  • Deviations accepted as named in the body:
    • Item tools stay read-only while customizing.
    • "Add view" stays off on a read-only base (N-19).
    • A list view the server does not hold yet offers no Customize.
    • A walled admin's Restore is not pre-disabled; the server's verdict shows.
    • The first pair save carries If-None-Match: *.
  • Observations, not blocking:
    • Before this session has an ensure answer, reads and Publish address the pair listedCustomizationPairOf finds: the spec's customizationPackageId, kept only when the package list lists it. Publish therefore names a listed pair, not one the door answered. It creates nothing, and the first write still asks the door.
    • The card's item 2 names the layered effective body. The save sends the document the pillar holds: the draft, else its baseline.
    • The off-contract ensure message is English-only.
  • Out of scope / NOT MEASURED: a browser-driven run; other languages fall back to en for the 12 new keys. Skills-docs: not owed.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #28562 closes the card on merge. On landing, the seat unblocks Studio: email template and translation editors in the All metadata area — a read-only base's template or bundle is customized as an override in <base>.custom #28569, Studio: customize a read-only package's flow into <base>.custom under its own name — one ledger switch, the declaration's badge, "Restore package original" #28570 and Setup: retire Setup → Metadata behind redirects into Studio (datasource kept) and retire Packaged automation's flows half; Studio stops linking into Setup #28578 for domain:ui#1, and claims N-19 Studio: a base and its <base>.custom as one surface — "customized" / "added" marks, "only customizations", the pair folded under its base, New lands in the pair #28572 itself.

Generated by Claude Code

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/xl tooling

Projects

None yet

1 participant