Repository navigation
Commit d62d973
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)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb7ALAiYy5ZTzr6TSya1KhXp)_
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
- `pnpm --filter @object-ui/app-shell run type-check` (src + tests):
exit 0 at `6674886524`.
- All 291 `views/studio-design` test files: green at `6674886524` (2108
tests). The 85 tests outside it that read the studio namespace: green at
`4bff2978d7`. `packages/i18n` (94 files): green.
- The 34 derived gates (`dispatch-gates --commands`) ran at
`0513534c9f`: 34 of 34 exit 0, reconciled with `--ran` (0 NOT-MEASURED,
0 UNRUN). `check:sdui-manifest`, `check:nul-bytes`, `check:i18n-keys`,
`check:i18n-drift` and `check:i18n-dead-keys` were re-run at
`4bff2978d7`: exit 0. ESLint on the touched files reported 0 errors.
- Merged `origin/main` three times. The second merge brought #28668 (the
card #28567) and the third brought #28669 (the card #28573); both merged
clean. At the head `1ae8358eb7`: app-shell `type-check` exit 0; this
card's four test files, #28573's two Studio and catalog test files,
`packages/i18n` (94 files) and the zh wording census green;
`check-sdui-manifest`, `check:nul-bytes`, `check:i18n-keys`,
`check:i18n-drift` and `check:i18n-dead-keys` exit 0. No parallel fix of
this card is on main.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QsR2jQpUMdR7QmHKTFR8iB
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0b63f5f commit d62d973
11 files changed
Lines changed: 2183 additions & 51 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/views/studio-design
- i18n/src/namespaces/studio
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
378 | 378 | | |
379 | 379 | | |
380 | 380 | | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
381 | 403 | | |
382 | 404 | | |
383 | 405 | | |
| |||
391 | 413 | | |
392 | 414 | | |
393 | 415 | | |
| 416 | + | |
394 | 417 | | |
395 | 418 | | |
396 | 419 | | |
| |||
403 | 426 | | |
404 | 427 | | |
405 | 428 | | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
406 | 435 | | |
407 | 436 | | |
408 | 437 | | |
| |||
494 | 523 | | |
495 | 524 | | |
496 | 525 | | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
497 | 536 | | |
498 | 537 | | |
499 | 538 | | |
| |||
505 | 544 | | |
506 | 545 | | |
507 | 546 | | |
508 | | - | |
509 | | - | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
510 | 551 | | |
511 | 552 | | |
512 | 553 | | |
| |||
558 | 599 | | |
559 | 600 | | |
560 | 601 | | |
561 | | - | |
| 602 | + | |
| 603 | + | |
562 | 604 | | |
563 | 605 | | |
564 | 606 | | |
| |||
574 | 616 | | |
575 | 617 | | |
576 | 618 | | |
577 | | - | |
| 619 | + | |
578 | 620 | | |
579 | 621 | | |
580 | 622 | | |
| |||
618 | 660 | | |
619 | 661 | | |
620 | 662 | | |
621 | | - | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
622 | 681 | | |
623 | 682 | | |
624 | 683 | | |
| |||
632 | 691 | | |
633 | 692 | | |
634 | 693 | | |
635 | | - | |
| 694 | + | |
636 | 695 | | |
637 | 696 | | |
638 | 697 | | |
| |||
1057 | 1116 | | |
1058 | 1117 | | |
1059 | 1118 | | |
| 1119 | + | |
1060 | 1120 | | |
1061 | 1121 | | |
1062 | 1122 | | |
| |||
1065 | 1125 | | |
1066 | 1126 | | |
1067 | 1127 | | |
| 1128 | + | |
| 1129 | + | |
| 1130 | + | |
| 1131 | + | |
| 1132 | + | |
| 1133 | + | |
1068 | 1134 | | |
1069 | 1135 | | |
1070 | 1136 | | |
| |||
1320 | 1386 | | |
1321 | 1387 | | |
1322 | 1388 | | |
1323 | | - | |
| 1389 | + | |
1324 | 1390 | | |
1325 | 1391 | | |
1326 | 1392 | | |
| |||
0 commit comments