Skip to content

Commit e9e31e9

Browse files
refactor(studio)!: one word per item state on the All metadata tiles, headers and marks (#29784)
domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB <!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb1HqvWsgjFfhijKzb1h4T7b)_ Fixes #29705 Clause-②: no (narrowing) Metadata-protocol: none Skills-docs: none — the four retired keys are rows of the Studio designer's own string table; no `skills/**`, `ui/skills/**` or docs page names them (grep at `601832b1` finds them only in package CHANGELOGs) ## 维护者速读 - **改了什么**:Studio「全部元数据」的类型卡片、列表和条目标题上的状态徽标、以及侧栏标记,现在统一用各板块已有的那组词:只读、可写、可定制、可新增、已定制、新增。只读软件包(代码包或已安装的包)里可以定制的类型显示「可定制」,不再显示「可写」;其它类型显示「只读」。页面顶部的说明改为告诉你在这里能做什么,不再数「协议暴露了多少类型」,筛选按钮改为「隐藏只读」。「权限」板块的共享设置去掉了中英重复(如 `Private 私有` 改为 `私有`),矩阵的 `OWD` 列名改为「记录共享模型」。 - **为什么改**:以前同一个状态在卡片、徽标和板块里各说各的,只读包里明明不能直接改的类型却写着「可写」,用户会以为可以原地编辑。 - **风险与回滚**:只改文案和徽标对应关系,没有新状态、没有新的元数据键或选项,保存、定制、发布的行为都不变。Studio 字符串表退役了 4 个键(下表),工作区内已无读取者。回滚即 revert 本 PR。 - **先看**:`ui/packages/app-shell/src/views/metadata-admin/PageShell.tsx` 的 `typeStateWord` 与 `TYPE_STATE_WORD_KEY`(卡片、列表标题和条目标题共用这一处);`DirectoryPage.stateVocabulary-29705.test.tsx`。 - **两分钟验证**:中文界面打开 Studio → ObjectStack Showcase → 全部元数据:仪表板、报表、视图、邮件模板显示「可定制」,其余显示「只读」,没有「可写」或「仅可新建」;打开「视图」列表,标题徽标同样是「可定制」。 - **需要你做的**:无,席位落地。 ## What changes - **One word per state, one key per word.** The All metadata tiles, the list and item headers (`WritabilityBadge`), the rail marks and the landing draw the words the pillars already use. The mapping is one function, `typeStateWord` in `PageShell.tsx`, and one key table, `TYPE_STATE_WORD_KEY`. The tile and the header both call them, so they cannot disagree. - **A tile answers "what can I do here" for the open package.** - On a read-only package, a type that takes a customization (an `allowEnvironmentOverlay` type, outside the `OS_METADATA_WRITABLE` hatch, and overridable in the spec's classification) reads **Can be customized**. Every other type there reads **Read-only**. - On a writable package, `~all` and Setup, an overlay type reads **Writable** and a create-only type reads **Can be added**. A type with neither flag reads **Read-only**. - **The lead sentence says what a person can do.** `engine.directory.description` no longer counts protocol types or says "runtime override". The "Writable only" filter becomes "Hide read-only" and counts the types whose word is not Read-only. - **The flow status keeps en "Off".** zh already reads 已停用, the same word as the landing's count. Moving en to "Switched off" is left to #29729, because the en-drift gate needs a waiver for an en-only change, and its waiver ledger accepts only `objectui#` issue numbers. - **Access: zh values only.** The sharing settings drop their English doublets (`Private 私有` → `私有`, `Public read 公共只读` → `公共只读`, `Public read/write 公共读写` → `公共读写`, `Controlled by parent 受父级控制` → `受父级控制`). The matrix's bare `OWD` column reads 记录共享模型, and the explain row reads 记录共享基线(OWD). English is unchanged. ### The vocabulary | State | Key | en | zh | |:---|:---|:---|:---| | read-only | `engine.studio.pkg.readonly` | Read-only | 只读 | | writable | `engine.studio.pkg.writable` | Writable | 可写 | | can be customized | `engine.studio.customize.available` (new) | Can be customized | 可定制 | | can be added | `engine.studio.customize.addable` (new) | Can be added | 可新增 | | customized | `engine.customizationMark.customized` | Customized | 已定制 | | added | `engine.customizationMark.added` | Added | 新增 | | switched off | `engine.studio.auto.off` | Off (Switched off in #29729) | 已停用 | Retired keys (zero `t()` readers after this change, per the `i18n-key-retirement` recipe): | Removed key | Replacement | |:---|:---| | `engine.badge.readOnly` | `engine.studio.pkg.readonly` | | `engine.badge.writable` | `engine.studio.pkg.writable` on a writable package, `engine.studio.customize.available` on a read-only one | | `engine.badge.createOnly` | `engine.studio.customize.addable` | | `engine.directory.writableOnly` | `engine.directory.hideReadOnly` (new) | ## Tests - `DirectoryPage.stateVocabulary-29705.test.tsx` (new, 29 tests) runs the real `MetadataDirectoryPage` and `ResourceListPage` under `MetadataAreaHostContext`, in zh with the studio catalogue loaded. - A read-only package: the overlay tiles read 可定制, the create-only and closed tiles read 只读, and no tile reads 可写 or 仅可新建. CONTROL: the same registry on a writable package reads 可写 / 可新增 / 只读, and a hatched (`overrideSource: 'env'`) type reads 可写. - The filter reads 隐藏只读 (3) on the read-only package and (5) on the writable one. The lead sentence has no digits and no 协议 or 运行时覆盖. - The list header draws the tile's word: the view list reads 可定制 and the page list 只读 on a read-only package. - The census: each state's single key with its en and zh, the keys that must say the same word, the retired keys and spellings absent from both packs, and `typeStateWord` over every context (permission reads Read-only everywhere, because it is not overridable). - Access in zh: the sharing values carry no Latin script, `colOwd` reads 记录共享模型, and en is unchanged. ### Moved pins These tests pinned the old words or found the header badge by its text. Each now pins the new word, or finds the badge by `data-state-word` or the package chip by its title. The seat approved the last three files, which were outside the first surface. - `DirectoryPage.environmentOverlay-12141.test.tsx`: 'Writable' / 'Read-only' / 'Hide read-only (6|0)'. - `PageShell.narrowHeader-12306.test.tsx`: 'Can be added'. - `packagedItemWriteGate-12169.test.tsx`: 'Writable'. - `PermissionMatrixEditor.readonlyHeaderBadge.test.tsx`: the badge is found by `[data-state-word="readOnly"]`. - `PermissionMatrixEditor.artifactTier.test.tsx`: 'Can be added' / 'Writable'. The package chip is found by its 'Read-only package' title. - `inspectors/inspectorStrings.studioNamespace-25922.test.ts`: the `'engine.badge.': 3` floor is removed (the family retired). - `PermissionMatrixEditor.readonly.test.tsx` and `PermissionMatrixEditor.cloneToCustomize.test.tsx` (seat-approved): the header badge and the identity strip's chip now both draw Read-only from one key, so the package chip is found by its 'Read-only package' title. - `metadataAreaHost.gates-28561.test.tsx` (seat-approved): under a read-only host, the translation header reads `engine.studio.customize.available` with its title. ## Acceptance notes - **Deviation, accepted by the seat:** on a read-only package, a create-only type reads **Read-only**, not "Can be added". The All metadata area offers no create on a read-only host (`ResourceListPage`'s `canCreate` requires `!host.readOnly`), and the list header there must agree with the tile. - **Access, how it was read:** OWD, the C/R/U/D/E/Tr/VA/MA letters and Private / Public read are sharing settings of an object, not states of an item, so they are not words in this vocabulary. Only their zh values changed, to drop the English doublets. - **The permission matrix header** was checked live in zh. 只读 shows once in the breadcrumb chips and once at the right of the set's row, never twice side by side. No change was needed (screenshot below). - **Out of scope, card #29729:** - The FlowLedgerSwitch's 本部署中已关闭 against 已停用. - The Customize verb 自定义 against the state 定制. - `perm.badge.custom` 自定义. - The CRUD legend letters C/R/U/D/E/Tr/VA/MA. - en "Switched off" for `engine.studio.auto.off` / `offTitle` (en still reads "Off" / "Disabled — will not run"). - The matrix's `OWD ` row-chip prefix, a literal in `PermissionMatrixEditor.tsx`. - `StudioItemHeader.tsx` is untouched. ## Verification Head `624a4f8f` adds a comment-only fix to the `tFormat` docstring example in `metadata-admin/i18n.ts` (it named `count` and `writable` on `engine.directory.description`; it now names `engine.directory.itemCountTooltip`) and merges `origin/main` `601832b1` (no overlap with this diff). `5472e3cb` keeps en "Off" (commit `6a4ea44a`, after CI's Type Check UI went red on `check-i18n-en-drift` at `d917cfef`) and merges `origin/main` `946a173f`. On `5472e3cb`: `check-i18n-en-drift`, `check:i18n-drift`, `check:i18n-keys`, `check:designer-field-key-parity`, `check-sdui-manifest`, `check-changeset-no-major` and `check-empty-changeset` exit 0; the census file, `FlowStatusDot.test.tsx`, main's new `draftObjectFlowGroup-29706` file and the `packages/i18n` suite: 98 files, 1482 passed, 13 skipped. The gates, the dogfood battery and the i18n suite were measured on `8d590d21`, which merges `origin/main` `3eeb81d1`. Head `d917cfef` merged `origin/main` `e82bf642`. No commit in that merge touches the studio packs, `DirectoryPage.tsx` or `PageShell.tsx`. Main's `cba12d22` and `7ba8b11f` touch `PermissionMatrixEditor.tsx` and its tests, but not its badge or chip. On `d917cfef` the derived gate list is unchanged (35). These were rerun on that head: - The changed test files plus main's two new matrix test files: 13 files, 331 tests passed. - `check-sdui-manifest`, `check-changeset-no-major` and `check-empty-changeset`: exit 0. <details><summary>Premise, the new tests and the ablations</summary> - Premise on `7b55ea95` (main), a temporary test (not committed) over the real directory under a read-only host with five types: it drew 可写 on the dashboard, report and view tiles and 仅可新建 on page. The lead sentence counted 5 types, "4 of which support runtime overrides". - Ablations with `scripts/ablation-replace.mjs`. Each was restored, the blob equals HEAD, and `git diff HEAD` is empty. - `if (packageReadOnly)` → `if (packageReadOnly && (false as boolean))` in `typeStateWord`: 3 tests failed (the read-only tiles, the list header and the mapping census). - The tile's `readOnly={packageReadOnly}` → `readOnly={false && packageReadOnly}`: 1 test failed (the read-only tiles). </details> <details><summary>Real browser: zh-CN screenshots of All metadata, a pillar and Access</summary> `examples/app-showcase` with the Console's Vite dev server on app-shell source, Playwright (Chromium), zh-CN, package `com.example.showcase` (read-only). The before leg served the four source files from `origin/main`, restored by trap, and the blobs were proven equal to HEAD. - Before: dashboard, report, view and email_template read 可写. Object, flow, page, action, permission, position and seven more types read 仅可新建. Lead: 平台协议共暴露 21 个元数据类型(其中 17 个支持运行时覆盖)。点击任意卡片即可浏览、覆盖或创建实例。 Access: `Public read/write 公共读写`. - After: dashboard, report, view and email_template read 可定制, and every other type reads 只读. Lead: 打开一个类型即可查看其中的项。每个类型都标明你在这里能做什么:只读、可定制、可新增或可写。 The view list header reads 可定制. Access shows the 记录共享模型 column and 公共读写 / 私有 / 受父级控制. - Files: `before-` and `after-` `all-metadata`, `all-metadata-full`, `view-list`, `pillar-automations`, `access` (.png and .json), plus `after-permission-matrix-header.png`. </details> <details><summary>Sweep, type-check and gates</summary> - Neighbour sweeps under the verify lock, every ui test file that names a changed key, spelling or component: 149 files, then 150 more. The first runs failed 7 tests in 4 files: three matrix files found Read-only twice once the header badge and the package chip shared a key, and the area header read the new word. Those files were repinned (listed above), and every file is green. On `8d590d21`, the changed test files, main's two new Access-pair test files and the `packages/i18n` suite: 108 files, 1771 passed, 13 skipped. - `turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n`: 66/66 tasks. - `dispatch-gates --commands`: 35 derived families, all exit 0, including `check-sdui-manifest` and `pnpm --dir ui exec vitest run scripts/` (170 files passed, 2 skipped). `pnpm check:new-line-citations` (ui): 0 new citations. `dispatch-gates --ran`: 35 derived, 35 run, 0 NOT-MEASURED. `check-closing-keyword-parity`, `check-changeset-no-major` and `check-adr-0087-registration` were also run against this body. - `clause2-line.mjs` is a library with no CLI. Its `readClause2Line` reads the changeset as `no` / `narrowing`, and `readMetadataProtocolLine` reads it as `none`. These match this body. - Dogfood battery (owed for the narrowing arm), `pnpm --filter @objectstack/dogfood test` under the lock after a full build on `8d590d21`: 402 files passed, 3 skipped; 3317 tests passed, 28 skipped. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jUqduUwnb8TeMn2R3D)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72877bc commit e9e31e9

16 files changed

Lines changed: 545 additions & 142 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@object-ui/i18n': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
refactor(i18n, app-shell)!: Studio names an item's state with one word everywhere
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) the four retired keys are rows of the Studio designer's own string table (the studio namespace of @object-ui/i18n, typed as a plain string record, outside the locale packs and TranslationKeys); no packages/spec schema, stored shape or metadata names them, and after this change no t() call in the workspace reads them -->
12+
13+
The All metadata tiles, the list and item headers, the rail marks and the landing now use the words the Studio pillars use for an item's state: Read-only, Writable, Can be customized, Can be added, Customized and Added (zh: 只读, 可写, 可定制, 可新增, 已定制, 新增).
14+
15+
A tile's word answers "what can I do here" for the open package. On a read-only package (a code or installed package), a type that takes a customization reads "Can be customized" instead of "writable"; any other type there reads "Read-only", because the All metadata area offers no create on a read-only package. On a writable package, an overlay type reads "Writable" and a create-only type reads "Can be added". The `OS_METADATA_WRITABLE` hatch opens nothing on a read-only package, so its types read "Read-only" there. The list and item headers draw the same word as the tile.
16+
17+
The All metadata lead sentence now says what a person can do, without counting protocol types, and the "Writable only" filter is now "Hide read-only".
18+
19+
In zh, the Access sharing settings drop their English doublets: `Private 私有` reads `私有`, `Public read 公共只读` reads `公共只读`, `Public read/write 公共读写` reads `公共读写`, `Controlled by parent 受父级控制` reads `受父级控制`, and the matrix's bare `OWD` column reads `记录共享模型`. English is unchanged.
20+
21+
**BREAKING** (`@object-ui/i18n`): four keys of the Studio namespace retire. A `t()` call that still names one reads the key back.
22+
23+
| Removed key | Replacement |
24+
|:---|:---|
25+
| `engine.badge.readOnly` | `engine.studio.pkg.readonly` |
26+
| `engine.badge.writable` | `engine.studio.pkg.writable` on a writable package, `engine.studio.customize.available` on a read-only one |
27+
| `engine.badge.createOnly` | `engine.studio.customize.addable` |
28+
| `engine.directory.writableOnly` | `engine.directory.hideReadOnly` |
29+
30+
New keys: `engine.studio.customize.available`, `engine.studio.customize.availableTitle`, `engine.studio.customize.addable`, `engine.studio.customize.addableTitle`, `engine.directory.hideReadOnly`. `engine.directory.description` no longer takes the `count` and `writable` placeholders.

‎ui/packages/app-shell/src/views/metadata-admin/DirectoryPage.environmentOverlay-12141.test.tsx‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
*
1515
* - the five overlay types (`view`, `dashboard`, `report`, `translation`,
1616
* `email_template`), served as the S2 server writes them, are badged
17-
* writable and counted by "Writable only";
17+
* writable and counted by "Hide read-only" (objectstack#29705 renamed the
18+
* filter and capitalised the words: the pillars' Writable and Read-only);
1819
* - a type whose ONLY open door is the overlay flag — `agent` elevated by the
1920
* `OS_METADATA_WRITABLE` hatch, which grants no runtime create — is badged
2021
* writable and counted, so the disjunction reads the new field and not just
@@ -124,31 +125,31 @@ describe('DirectoryPage — the type tiles read `allowEnvironmentOverlay` (objec
124125
renderDirectory();
125126

126127
for (const type of OVERLAY_TYPES) {
127-
const badge = within(await tile(type)).getByText('writable');
128+
const badge = within(await tile(type)).getByText('Writable');
128129
expect(badge).toHaveAttribute('title', 'Writable per ADR-0005 overlay opt-in');
129130
}
130131

131-
const hatched = within(await tile('agent')).getByText('writable');
132+
const hatched = within(await tile('agent')).getByText('Writable');
132133
const title = hatched.getAttribute('title') ?? '';
133134
expect(title).toContain(HATCH_TITLE_FRAGMENT);
134135
expect(title).toContain('never an item a package ships');
135136
expect(title).not.toContain('OBJECTSTACK_METADATA_WRITABLE');
136137

137-
expect(within(await tile('api')).getByText('read-only')).toBeInTheDocument();
138-
expect(within(await tile('api')).queryByText('writable')).toBeNull();
138+
expect(within(await tile('api')).getByText('Read-only')).toBeInTheDocument();
139+
expect(within(await tile('api')).queryByText('Writable')).toBeNull();
139140

140-
// "Writable only" counts the five and the hatched type: six of seven.
141-
expect(screen.getByRole('button', { name: /Writable only \(6\)/ })).toBeInTheDocument();
141+
// "Hide read-only" counts the five and the hatched type: six of seven.
142+
expect(screen.getByRole('button', { name: /Hide read-only \(6\)/ })).toBeInTheDocument();
142143
});
143144

144145
it('the hatch-elevated type served WITHOUT the field is read-only and not counted', async () => {
145146
const { allowEnvironmentOverlay: _omitted, ...withoutField } = HATCHED_AGENT;
146147
served = [withoutField, API];
147148
renderDirectory();
148149

149-
expect(within(await tile('agent')).getByText('read-only')).toBeInTheDocument();
150-
expect(within(await tile('agent')).queryByText('writable')).toBeNull();
151-
expect(screen.getByRole('button', { name: /Writable only \(0\)/ })).toBeInTheDocument();
150+
expect(within(await tile('agent')).getByText('Read-only')).toBeInTheDocument();
151+
expect(within(await tile('agent')).queryByText('Writable')).toBeNull();
152+
expect(screen.getByRole('button', { name: /Hide read-only \(0\)/ })).toBeInTheDocument();
152153
});
153154
});
154155

@@ -165,27 +166,27 @@ describe('PageShell — the header badge reads `allowEnvironmentOverlay` (object
165166

166167
it.each(OVERLAY_TYPES)('%s: badged writable from the registry opt-in', (type) => {
167168
renderShell(overlayEntry(type));
168-
expect(screen.getByText('writable')).toHaveAttribute('title', 'Writable per ADR-0005 overlay opt-in');
169+
expect(screen.getByText('Writable')).toHaveAttribute('title', 'Writable per ADR-0005 overlay opt-in');
169170
});
170171

171172
it('a hatch-elevated type: the tooltip names OS_METADATA_WRITABLE and what it opens', () => {
172173
renderShell(HATCHED_AGENT);
173-
const title = screen.getByText('writable').getAttribute('title') ?? '';
174+
const title = screen.getByText('Writable').getAttribute('title') ?? '';
174175
expect(title).toContain(HATCH_TITLE_FRAGMENT);
175176
expect(title).toContain('never an item a package ships');
176177
expect(title).not.toContain('OBJECTSTACK_METADATA_WRITABLE');
177178
});
178179

179180
it('negative control: `api` without the field is badged read-only', () => {
180181
renderShell(API);
181-
expect(screen.getByText('read-only')).toBeInTheDocument();
182-
expect(screen.queryByText('writable')).toBeNull();
182+
expect(screen.getByText('Read-only')).toBeInTheDocument();
183+
expect(screen.queryByText('Writable')).toBeNull();
183184
});
184185

185186
it('the hatch-elevated entry served WITHOUT the field is not badged writable', () => {
186187
const { allowEnvironmentOverlay: _omitted, ...withoutField } = HATCHED_AGENT;
187188
renderShell(withoutField);
188-
expect(screen.queryByText('writable')).toBeNull();
189-
expect(screen.getByText('read-only')).toBeInTheDocument();
189+
expect(screen.queryByText('Writable')).toBeNull();
190+
expect(screen.getByText('Read-only')).toBeInTheDocument();
190191
});
191192
});

0 commit comments

Comments
 (0)