You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 797f925
Browse filesBrowse the repository at this point in the historyBrowse files
feat(app-shell): Studio customizes an installed package's page into its customization package (#30063) (#30233)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB7LEx1K2Cto3SeXFE6Vzmep)_Fixes#30063
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:已安装(只读)包里的页面,在
Studio「界面」中打开时现在会出现「定制」按钮;按下后可在页面设计器里编辑,首次保存把整页复制进 `包名.custom` 配对包并声明
`overrides: { package, basedOn
}`,发布后运行中的应用显示定制后的页面;「恢复原版」删除这份副本。导轨的「已定制」标记、「仅看定制」计数和「全部元数据」的页面卡片计数也都覆盖页面。
- **为什么改**:服务器早已支持页面覆盖(卡片实测过),只缺 Studio 的入口,客户想改欢迎页、首页只能等代码发版。
- **风险与回滚**:很小。只把 `page` 加进 Studio 可定制类型清单,首存、恢复、标记都走仪表板/视图现成的通用路径;无
spec、服务端、元数据类型改动;可写包的页面编辑不变。回滚即 revert 本
PR。定制后的页面是整份副本,厂商后续版本对该页的修改要恢复原版后重做才会出现。
- **先看**:`StudioCustomizeControls.tsx` 的 `STUDIO_CUSTOMIZE_TYPES`
一行;`InterfacesPillar.customize-28562.test.tsx` 新增的 "Customize a
read-only base’s page into its pair (objectstack#30063)" 一组测试。
- **两分钟验证**:在仓根运行 `pnpm --dir ui exec vitest run
packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx
-t 30063`,预期 5 条通过、其余跳过。
- **需要你做的**:无,席位落地。
## What changes
Studio's Customize door (`STUDIO_CUSTOMIZE_TYPES` in
`StudioCustomizeControls.tsx`) now lists `page` beside view, dashboard,
report and app. `page` is `overridable` in the spec's classification
(`isOverridableMetadataType`), so `isStudioCustomizeType('page')` now
answers true. That one line is the whole runtime change. Everything else
a page needs was already type-generic in the Interfaces pillar, and is
now reached for pages:
1. **Customize, first save, later saves.** A read-only base's page shows
Customize in its header (the leaf path `leafCustomizable`). Until it is
pressed, the designer edits nothing. The first saved designer edit asks
the ensure door once and writes the whole page into the pair
(`overrideDraftBody`: the page without its `_` keys, plus `overrides: {
package: BASE, basedOn: BASE_MANIFEST_VERSION }`), pinned to "no draft
there" (`ifNoneMatch: '*'`). Later edits save into the pair's copy at
the version they were answered. The base is never written.
2. **Designer edits reach the running app.** The page's draft sits in
the pair's pending changes; publishing it serves the customized page at
`/apps/APP/page/NAME`.
3. **Restore package original.** The header's override mark offers
Restore, which deletes the pair's draft and row of the page after a
confirmation; the shipped page reads back.
4. **Marks, "only customizations", All metadata tile.** These read the
server's customization ledger and are type-agnostic. With the page's
override served, the rail row carries the "customized" mark, "only
customizations" counts and lists it, and the All metadata page tile
counts it.
No spec, server or metadata-type change (card Acceptance 5): the browser
check below ran end to end against an unchanged server.
Out of scope, as the card says: a renamed page's label can be hidden by
the base's translation. That is filed as #30062 (its third finding) and
is not fixed here.
Diff: 244 changed lines (+233 / −11, 5 files), under the 1,500 split
line, so Acceptance 1–4 ship together.
## Tests
`InterfacesPillar.customize-28562.test.tsx` (the shared harness), new
top-level describe "Customize a read-only base’s page into its pair
(objectstack#30063)", 5 tests. The page designer is reduced to a
stand-in canvas (its first text block and one in-place edit), the way
the harness already stands in for the dashboard canvas:
- Customize is offered and writes nothing; the first designer edit asks
the door once (201) and writes the whole page into the pair with
`overrides: { package, basedOn: '4.0.0' }` and `ifNoneMatch: '*'`; the
base row is unchanged; the override chip appears; the next edit saves
into the pair at `ifMatch: 'v1'` without asking the door again.
- A served override opens as the pair's copy, editable, with no
Customize to press, and an edit saves into the pair.
- Restore deletes the pair's draft and row (`DELETE
…state=draft&package=PAIR`, then `DELETE …package=PAIR`), and the
shipped page reads back with Customize offered again.
- The rail marks the customized page (`data-kind="customized"`), "only
customizations" reads 1 and lists the page alone.
- CONTROL: on a writable package a page edit saves at the package, with
no door and no `overrides`.
Also:
- `StudioCustomizeControls.customize-28562.test.tsx`: `page` moves from
the "is not customized" list to the "is customized" list.
- `metadataAreaHost.customizations-28568.test.tsx`: the All metadata
page tile counts a page's override ("1 customized"), and the header's
total includes it.
- CONTROL for the other types: the existing dashboard, view, list-view
and app (navigation) describes in the same harness ran unchanged and
green.
<details><summary>Ablation (one-time, not kept)</summary>
Committed first, then `node scripts/ablation-replace.mjs` replaced
`'report', 'page', 'app'` with `'report', /* ablated-30063 */ 'app'` in
`StudioCustomizeControls.tsx` (anchor 1 → 0, blob `1452690e` →
`f2d008e3`) and ran `pnpm --dir ui exec vitest run
packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx
-t 30063` (ui vitest resolves `@object-ui/*` to src, so no build leg):
- Red: 3 failed, 2 passed. The first-save pin, the served-override pin
and the Restore pin failed (no Customize button, no override chip).
- Green as expected under the ablation: the rail-mark pin (marks read
the server's ledger, not this list) and the writable-package CONTROL.
- Restored: blob back to `1452690e` = HEAD, `git diff HEAD` empty (the
script's own check).
</details>
## Acceptance notes
- **Browser check (card Acceptance 4).** Dev boot of `examples/app-crm`
from this branch (`objectstack dev --seed-admin --fresh -p 4317`,
Console Vite on 5317 against it), admin, zh-CN, head `eee39383` (before
the `origin/main` merge; the merge brought no change to this PR's diff,
and the suites below were rerun after it). Screenshots are in the
project's `ui-orders/30063-shots/` folder; file names below.
1. `欢迎页` (`crm_welcome`) in Studio → Interfaces reads 只读 and now offers
定制 (`01-studio-page-readonly-offers-customize.png`).
2. 定制 → selected the main text block → changed 内容. The first save sent
`POST /packages/com.example.crm/ensure-customization` → 201, then `PUT
/meta/page/crm_welcome?mode=draft&package=com.example.crm.custom` → 200;
the header shows the override chip and "保存到 CRM (minimal example) 的定制"
(`02-first-edit-saved-into-custom-pair.png`).
3. 发布 lists one page change saved to the pair
(`03-publish-lists-the-page-in-the-pair.png`); 全部发布 sent `POST
/packages/com.example.crm.custom/publish-drafts` → 200.
4. `/apps/com.example.crm/page/crm_welcome` shows the edited text
(`04-runtime-serves-the-customized-page.png`). `GET
/packages/com.example.crm/customizations` then listed `{type: page,
name: crm_welcome, basedOn: 4.0.0}`.
5. The rail marks 欢迎页 "customized" and 仅看定制(1)lists it
(`05-rail-marks-the-page-and-only-customizations-counts-it.png`); the
All metadata page tile reads 已定制 1 and the header "CRM (minimal example)
已定制 1 项" (`06-all-metadata-page-tile-counts-the-customization.png`).
6. 恢复原版 asks first (`07-restore-package-original-confirm.png`), then
sent `DELETE …?state=draft&package=com.example.crm.custom` and `DELETE
…?package=com.example.crm.custom` (both 200). Studio shows the shipped
text with 定制 offered again
(`08-restored-shipped-page-customize-offered-again.png`), the runtime
serves the shipped page
(`09-runtime-serves-the-shipped-page-again.png`), the customization
listing is empty, and the base's page body read before and after is
identical.
- **Seen in the browser, not fixed here (reported to the seat for
filing):**
- After 恢复原版, the rail keeps the "customized" mark and 仅看定制(1)until the
page is reloaded (visible in `08-…png`; a reload shows 0 marks and no
filter). The marks read (`usePackageCustomizations`) re-reads only on
`publishNonce`, and Restore does not bump it. That code path is shared
with dashboards, views and the app, so it is not page-specific.
- The All metadata page tile's state word reads 只读 while Studio now
customizes pages (`06-…png`); the app tile reads the same since #30059.
The word comes from `typeStateWord`, which reads the type's
`allowEnvironmentOverlay`, not the customize door. Carrier: #30062 item
4 (branch `claude/project-thread-yenpdr`) makes `typeStateWord` in
`PageShell.tsx` read the customize door's own type table
(`isStudioCustomizeType`). Serial: whichever of the two PRs lands second
updates the #29705 test's tile expectation for `page` from 只读 to 可定制;
this PR does not touch `PageShell.tsx`.
- The page designer in this PR's pins is a stand-in canvas. The real
block designer's save into the pair is the browser check above.
- **Open PRs on the same files, re-checked at PR time.** #30224 (#30064,
dataset Customize) edits the same `STUDIO_CUSTOMIZE_TYPES` doc comment
in `StudioCustomizeControls.tsx`: the two hunks overlap, so whichever
lands second merges `origin/main` and keeps both sentences (the page in
the types list, the dataset in the area editor). #30226 (#30062) edits
`PageShell.tsx` (see the tile note above); this PR does not touch it.
#30211 edits `StudioDesignSurface.tsx`, which this PR does not touch.
## Verification
All under `scripts/pm/os-verify-lock.sh` unless noted. Head `a73ecd64`
(`origin/main` `dfafae81` merged, clean) for the rows marked "after the
merge"; the first rows ran on `eee39383`.
- Build: `turbo run build --filter='@object-ui/app-shell...'`, 68/68
tasks; then the full build (`--filter=!@objectstack/docs`), 119/119, for
the dev boot.
- The three touched test files: 3 files, 73 tests, all passed.
- The whole `packages/app-shell/src/views/studio-design/` directory: 324
files, 2397 tests, all passed. After the merge (full build 119/119
first), that directory plus
`metadataAreaHost.customizations-28568.test.tsx`: 325 files, 2415 tests,
all passed.
- `@object-ui/app-shell` `type-check` (both tsconfigs) → exit 0, before
and after the merge.
- `npx eslint` on the 4 touched source/test files (unlocked): 0 errors,
15 warnings (all `react-refresh/only-export-components` on existing
exports of `StudioCustomizeControls.tsx`).
- `node scripts/check-sdui-manifest.mjs` → exit 0.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 35 commands (the same 35 before and after
the merge), each run on `a73ecd64` and recorded `:: exit 0`; `--ran` →
"35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN". Record: project file
`ui-orders/30063-gates-ran.list`.
- Changed lines vs the human-merge threshold: 244 (+233 / −11, 5 files),
under 3,000.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzhKo5SDER9fGbQQqSa)_
Co-authored-by: Claude <noreply@anthropic.com>
feat(app-shell): Studio customizes an installed package's page into the base's customization package (#30063)
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
On a read-only (installed or code-shipped) package, a page opened in the Interfaces pillar now offers Customize, the same button a dashboard, report or view shows. Before, Studio showed the page as read-only with no way to change it, although the server already serves a page override from the paired customization package (`<base>.custom`).
11
+
12
+
- Customize unlocks the page designer. Nothing is written until an edit is saved. The first saved edit copies the whole page into the pair, declaring `overrides: { package: <base>, basedOn: <base manifest version> }`. Later edits update that copy. The base package is never written.
13
+
- The edit is a draft in the pair's pending changes; once published, the running app serves the customized page at its usual address.
14
+
- The page's header carries the override mark, whose popover offers "Restore package original": it deletes the pair's draft and copy of the page after a confirmation, and the shipped page reads back.
15
+
- The Interfaces rail marks a customized page, "only customizations" counts and lists it, and the All metadata directory's page tile counts it.
16
+
17
+
A customized page is a whole copy: a change the vendor ships to that page in a later version does not appear until the customization is restored and redone. A writable package's page editing is unchanged. No spec, server or metadata-type change.
describe('Customize a read-only base’s page into its pair (objectstack#30063)',()=>{
926
+
it('Customize is offered and writes nothing; the first designer edit copies the whole page into the pair, declaring the override; the base is untouched',async()=>{
0 commit comments