Repository navigation
Commit 7253344
feat(studio): a read-only base and its customization pair as one surface (#28747)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbPFWt6rX5ZYoSCwDefB5cHK)_
Fixes #28572
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:Studio 把只读基础包和它的定制包 `.custom`
合成一个界面。四个栏位(数据、自动化、权限、界面)在同一个列表里同时列出基础包的项和定制包新增的项,并标出「已定制」或「新增」。每个栏位多了一个「仅看定制」开关。包切换器不再单独列出定制包,对子地址会跳到基础包。在只读基础包上点「新建」,会经由
ensure 接口建到定制包里。
- **为什么改**:ADR-0146 D3 要求「一个界面」。之前定制包在切换器里像另一个包,作者要来回切换,也看不出哪些项被定制过。
- **风险与回滚**:只改了 `app-shell` 的 studio-design 目录和 studio
文案(en/zh),没有改协议、服务端或 spec。可写包,以及没有定制包的只读包,读取和显示都和原来一样(有对照测试)。回滚就是 revert
这个 PR。
- **先看**:`StudioRailCustomizations.tsx`(共用的合并、标记、筛选和深链兜底逻辑),然后看
`StudioDesignSurface.tsx` 里的 `StudioDesignSurface` 外壳(对子地址跳转,以及 metadata
区不跳转)和 `InterfacesPillar` 的 `doCreateSurface`(新建进对子)。
- **两分钟验证**:`cd ui && pnpm exec vitest run
packages/app-shell/src/views/studio-design/StudioDesignSurface.oneSurface-28572.test.tsx`,10
条全过。
- **需要你做的**:不需要拍板。第三方包贡献的项(扩展字段、基础对象上的流程、D7
导航条目)没有画出来,因为没有任何已服务的读取说明是哪个包贡献的。这部分的服务端工作见 the card #28722。
## What changes
Studio now shows a read-only base and its paired customization package
(`<base>.custom`) as one surface (ADR-0146 D3, "On top, one surface").
Everything is in `ui/packages/app-shell/src/views/studio-design/**` and
the studio i18n namespace.
- **One list per rail.** The Data, Automations, Access and Interfaces
rails list the base's items, then each item of the pair whose name the
base does not hold, stamped `addedIn: <pair>` (`withPairItems`). An
override the pair serves under the base's name stays the base's row, as
ADR-0146 D8 item 2 serves it. The Interfaces rail does this for pages,
dashboards, reports and views (its off-nav sections).
- **Marks.** Rows are marked from the base's N-15 read (`GET
/api/v1/packages/:id/customizations`) through the one rule
`customizationMarks.ts` already holds. An override the read lists is
"customized". Any other row of the pair is "added". Every other row has
no mark. Nothing is computed from the documents. A failed read marks
nothing and shows the server's sentence under the rail's search. The
read refreshes on Publish.
- **Only customizations.** Each rail gets an "Only customizations (n)"
toggle under its search (`StudioRail`'s new `filters` slot), shown once
some row carries a mark. On Interfaces it lists every marked item in
place of the nav tree, including ones a nav entry opens.
- **Package switcher.** A pair whose base is listed has no row of its
own. The base's row names it ("with `<pair>`")
(`foldCustomizationPairs`).
- **The pair's URLs.** `/studio/<pair>/<pillar>` lands on
`/studio/<base>/<pillar>`, with its query and router state kept. The
pairing comes from the spec's rule (`customizationBasePackageId`), so
the redirect never waits on the package list. A `?surface=` link to an
item the pair adds opens it once the pair is known
(`useDeepLinkFallback`). The rail's first read happens before the
package list answers, so it opens its default first. It then switches to
the linked item if the author has not moved off that default.
- **Items the pair adds** open writable on the base's surface. They are
read and saved at the pair their row was listed in (`Surface.addedIn`
and its siblings in the Data, Automations and Access pillars). They are
saved as they are, with no override declaration. They show no Customize
and no read-only link, and the item header names the pair
(`StudioPairItemChip`).
- **New on a read-only base** goes through the ensure door
(`StudioBaseCustomization.ensurePackage`) in all four pillars. It then
saves into the package the door answered, with the door's `namespace`
where a name is prefixed (objects). After the create, each rail
re-reads, so the new row is listed as added. On Interfaces, the "+"
offers only page, dashboard and report there. The created item links no
nav entry, because the base's app takes no edit. The dialog says where
it lands. A walled admin is offered no New on a read-only base.
- **Unchanged.** A writable package, and a read-only base with no pair,
read and draw as before. Customized items keep the card #28562 Customize
path (override declaration, badge, Restore).
- **Strings.** Five keys go in the studio namespace (`en`, `zh`). That
namespace is typed `Record<string, string>` and sits outside the locales
packs, so `Clause-②: no` holds as claimed.
### Named decisions and deviations
1. **Contributed items are not drawn.** No served read names the package
that contributes extension fields, flows on the base's objects, or
ADR-0029 D7 nav entries. The registry's merged object carries no
per-field source, and flow and nav reads name none either. Nothing
third-party is greyed or listed. The test `nothing a third package
contributes is drawn` pins that no rail asks another package's list and
no row names one. The server side is the card #28722.
2. **Write target.** A create names only the pair the ensure door
answered. An edit of an added item names the pair its row was listed in
(`addedIn`, taken from the read that listed it). ⛔ No write names a
`.custom` id spelled in the browser. The spec's pairing rule is used for
READS only: the redirect, and matching a "New similar" arrival to the
rail (`pairIdOf`).
3. **`/studio/<pair>/metadata` is not redirected.** The "All metadata"
area lists the pair's items under `?package=<pair>` and re-roots there,
so redirecting it would loop. The test `the pair's "All metadata" URL
stays on the pair` pins this. Ablating the exemption reddens it.
4. **Extended base objects carry no rail mark.** A field the pair adds
to a base object makes the object neither an override nor an added item
under the one mark rule. The extension count is on the All metadata
directory (the card #28568). The Data test pins the object as unmarked.
5. **An object create on a read-only base asks the door before the name
check.** The name's namespace prefix is the door's answer, so a refused
or taken name can still leave a newly created, empty pair behind.
6. **Known gap, not widened.** The report dialog's "New dataset" on a
read-only base saves into the open package, which is the base while
creating, so the server refuses it as before. This PR does not route
datasets.
## Tests
- New: `StudioDesignSurface.oneSurface-28572.test.tsx`, 10 tests. It
mounts the real `StudioDesignSurface` and metadata client over a fetch
stub. Covered:
- the customized and added marks, and no mark on the base's own rows;
- the filter showing exactly those two rows;
- an added item saving at the pair with no override, and the door not
asked;
- New on the read-only base asking the door (`200`), saving into the
pair, with no app save;
- the Data rail's added object, and its header chip;
- the switcher with no pair row and the base naming the pair;
- the pair URL landing on the base and opening the linked added item;
- the metadata area not redirected;
- no third-party read or row;
- CONTROL: a base with no pair reads no pair list, and draws no mark and
no filter.
- Updated: `InterfacesPillar.customize-28562.test.tsx` (its host passes
the new `ensurePackage` and `marks`) and
`StudioCustomizeControls.customize-28562.test.tsx` (the door answer now
carries `namespace`).
- Ablations, each run through `scripts/ablation-replace.mjs` against the
new file, with the file restored to HEAD each time. Each went red:
- the metadata exemption;
- the pair redirect;
- the `addedIn` stamp;
- the switcher fold;
- New's pair target;
- the added item's save target;
- the filter;
- the deep-link upgrade;
- the pair read.
## Acceptance notes
- Wanted 1: one list per rail, with marks from N-15 plus the pair's
list. Done in all four pillars.
- Wanted 2: a per-rail filter. Done.
- Wanted 3: the switcher fold, and a pair URL that lands on the base.
Done, with the metadata exemption (decision 3).
- Wanted 4: New on a read-only base lands in the pair through the door,
and the header names the pair. Done. There is no Publish path of its
own: Pending and Publish already act on the pair (the card #28562).
- Wanted 5: contributed items. Not drawn, and the kinds are listed under
decision 1, with the server side cut as the card #28722.
- Wanted 6: pinned by the new test file and its ablations.
## Verification
- `pnpm --filter @object-ui/app-shell --filter @object-ui/i18n run
type-check` (source and `tsconfig.test.json`): exit 0. ESLint on the
touched files: 0 errors. `check:i18n-keys`, `check:i18n-drift` and
`check:i18n-dead-keys` (from `ui/`): exit 0.
- The studio-design suite (288 files) plus `studio-locale.i18n` and
`zh-base-term.i18n-13131`, from `ui/` under `os-verify-lock`: all
passed. Because the deep-link fallback landed after the first three
chunks ran, the 14 files there that touch `?surface=` links were run
again at the final code: 14/14.
- `packages/i18n/` (94 files): passed.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 34 commands, run and reconciled with
`--ran` (results in the card report).
- Merged `origin/main` twice (7 commits, then 5, no conflict). Neither
range touches `studio-design/**` or the studio namespace, so there is no
parallel same fix. The gate sweep and the suites ran at 65e3b1b, and
`check-sdui-manifest` was re-run at the head.
---
https://claude.ai/code/session_01QsR2jV3gDN72FtkqvGmfBd
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a9782b3 commit 7253344
14 files changed
Lines changed: 1386 additions & 155 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 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
97 | 101 | | |
98 | | - | |
| 102 | + | |
99 | 103 | | |
100 | 104 | | |
101 | 105 | | |
| |||
119 | 123 | | |
120 | 124 | | |
121 | 125 | | |
122 | | - | |
| 126 | + | |
123 | 127 | | |
124 | 128 | | |
125 | 129 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
262 | 262 | | |
263 | 263 | | |
264 | 264 | | |
265 | | - | |
| 265 | + | |
266 | 266 | | |
267 | 267 | | |
268 | | - | |
| 268 | + | |
269 | 269 | | |
| 270 | + | |
270 | 271 | | |
271 | | - | |
272 | | - | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
273 | 277 | | |
274 | 278 | | |
275 | 279 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | | - | |
| 209 | + | |
210 | 210 | | |
211 | 211 | | |
212 | | - | |
| 212 | + | |
213 | 213 | | |
| 214 | + | |
214 | 215 | | |
215 | | - | |
216 | | - | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
217 | 221 | | |
218 | 222 | | |
219 | 223 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
| 140 | + | |
140 | 141 | | |
141 | 142 | | |
142 | 143 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
| 248 | + | |
| 249 | + | |
248 | 250 | | |
249 | 251 | | |
250 | 252 | | |
| |||
0 commit comments