Skip to content

feat(studio): edit an email template or a translation in the All metadata area, and customize a read-only package's one per locale - #28743

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

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-mtqfy5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #28569

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio「All metadata」区里,email_template 与 translation 不再落到通用条目页,而是打开一个简化表单编辑器:Setup 表单的字段(served schema,缺省用内置 schema)+ 该类型已注册的预览(EmailTemplatePreview / TranslationPreview,挂载复用,非拷贝)。可写包写自己的草稿;只读基座走 N-17 的 Customize 路径(ensure → 草稿写入 <base>.custom 并声明 overrides: {package, basedOn} → 头部 Publish 发布 pair → ⋯ 菜单「Restore package original」)。
  • 为什么改:此前改一个已安装包的邮件模板或翻译只能去 Setup,写出的是不属于任何包的行;本卡让它成为 pair 里的声明式覆盖(ADR-0146 D2),且邮件模板按 (name, locale) 逐语言覆盖,翻译整包替换并在首次保存前明说。
  • 风险与回滚:改动只在 ui 的 app-shell(新增 StudioBundleItemEditor.tsx,StudioMetadataArea 的条目路由多一个分支,useServedOverride 多一个可选 member)与 studio 命名空间三条字符串;服务端零改动。Setup 页面、跨包 ~all 腿、?scope=environment、?review=1 仍走通用条目页。回滚 = revert 本 PR。
  • 先看:ui/packages/app-shell/src/views/studio-design/StudioBundleItemEditor.tsx 的 doSave(只读基座分支:ensure 一次、overrideDraftBody、recordRead(..., {ifNoneMatch:'*'}))和 restoreOriginal(先删 pair 草稿再删行,均带 member);再看 metadataArea.ts#areaItemOpensFormEditor。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/app-shell/src/views/studio-design/StudioBundleItemEditor.customize-28569.test.tsx packages/app-shell/src/views/studio-design/StudioMetadataArea.routes-28561.test.tsx packages/app-shell/src/views/studio-design/StudioDesignSurface.metadataArea-28561.test.tsx,应全部通过。
  • 需要你做的:无需决策;正常评审即可。Acceptance notes 里有一条与 Interfaces 柱一致的已知行为(Restore 后头部待发布计数不即时刷新)。

What changes

  • Routing. metadataArea.ts gains AREA_FORM_EDITOR_TYPES (email_template, translation) and areaItemOpensFormEditor({packageId, type, search}): true inside one package, outside ?scope=environment and ?review=1. StudioMetadataArea's item route (AreaItemOpen) decides once: the form editor, else the pillar, else MetadataResourceEditPage as before. The item is keyed by type/name/member, so another locale is another editor (the draft-save guard's version pin is keyed by type, name and package, not member).
  • The editor (StudioBundleItemEditor.tsx, new). SchemaForm over the served schema, else the default schema (as the generic item page resolves it), with name and overrides hidden, and locale too for an email template. Beside it, the type's registered preview. Autosave through useDraftAutoSave and the shared useDraftSaveGuard (same version pin and conflict dialog as the pillars). Header: type, label, locale chip, override badge, autosave status, Customize, ⋯ (History, Restore package original).
  • A read-only base. Customize (StudioCustomizeButton) only unlocks the form. The first saved edit calls the surface's ensure() once, saves the draft into the pair with overrides: {package, basedOn} (overrideDraftBody), and bumps the badge. Restore (StudioRestoreOriginalDialog) deletes the pair's draft, then its row, at the item's member, then re-reads the base. A walled caller sees Customize disabled with the reason.
  • Per member. Every read, save, badge read and restore of an email template carries ?member=<locale> from the list row's link (BUNDLE_MEMBER_PARAM). useServedOverride takes an optional member and passes it to getDraft / layered. A translation has no member; before the first save on a read-only base the editor says the whole bundle is replaced. For an email template it says only this locale is customized.
  • Surface. StudioDesignSurface passes its existing customization, publishNonce and onDraftSaved to StudioMetadataArea; nothing else in the surface moves.
  • Strings. Three engine.studio.area.formEditor.* keys in the studio namespace (en, zh). That table is Record<string, string> outside the locales packs, so Clause-②: no holds as claimed.

Tests

  • StudioBundleItemEditor.customize-28569.test.tsx (new, 6): writable draft of one member (no door, no declaration, no _ keys, locale field hidden, preview live); read-only Customize of email template zh-CN (ensure once, pair draft body exact with the declaration, badge, base untouched, pair read at member zh-CN); the other locale (en-US) untouched; Restore deletes the pair's draft and row of zh-CN alone and the base reads back; walled admin sees Customize disabled; translation whole-bundle note then the whole bundle saved into the pair, no member in any read.
  • StudioMetadataArea.routes-28561.test.tsx (+4): writable translation and read-only email template at member zh-CN open the editor; ?review=1 and ~all open the generic page.
  • StudioDesignSurface.metadataArea-28561.test.tsx (+2): the surface hands the area a read-only base's customization (base, baseLabel, basedOn, walled, pairId, ensure), and null for a writable package, whose saved draft moves the changes count.
Ablations (6, through scripts/ablation-replace.mjs; each landed on disk, went red, and was restored to HEAD by the tool)
# Mutation Result
A1 the editor's layered read drops member editor file: 4 failed, 2 passed
A2 the badge read (useServedOverride) drops member 3 failed, 3 passed
A3 locale shown in the email template form 1 failed (writable case), 5 passed
A4 the pair draft saved without the overrides declaration 2 failed (email Customize, translation), 4 passed
A5 Restore's draft delete drops member 1 failed (Restore), 5 passed
A6 areaItemOpensFormEditor always false routes file: 2 failed, 18 passed

Acceptance notes

  • Restore package original does not refresh the header's pending count at once; it emits the metadata-refresh pulse, exactly as the Interfaces pillar's Restore (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) does.
  • An email template's draft PUT takes no ?member= (400 VALIDATION_ERROR, supported: force, mode, package, surface); the member is the body's locale. The editor sends the body it read, locale included and hidden from the form, so a save stays on its locale.
  • Server serving was checked before any code: the layered read and the served-override lookup are per member (overlayMemberWhere, memberOption(member)), so the card's stop rule ("server serves or gates a per-(name, locale) override wrongly") did not fire on reads. The write gate's shipper lookup is per (type, name); see the report's finding.

Verification

  • The three card test files: editor 6/6, routes 20/20, surface metadata-area cases green.
  • Wider set under scripts/pm/os-verify-lock.sh: every test file naming StudioCustomizeControls, useServedOverride, StudioMetadataArea, metadataArea, StudioBundleItemEditor or InterfacesPillar, plus every test importing the studio namespace (zh-base-term.i18n-13131, the count-family census among them): 159 files, 2064 tests passed, at 37edd173 (main's merge after it touches no ui file).
  • turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n: exit 0 (at 1f6a135c; the later commits add one lint comment and main's non-ui files). ESLint on the 10 changed ts/tsx: 0 errors (warnings are the files' existing ones plus react-hooks advisories).
  • node scripts/check-sdui-manifest.mjs: exit 0.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands) at ca3cc565: 34 of 34 run, each exit 0 (check:type-check-debt under the lock); --ran reconciles 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --dir ui exec vitest run scripts/: 169 files passed, 2 skipped.
  • Live, on objectstack dev --fresh for examples/app-showcase (base com.example.showcase 0.1.0, which ships showcase_task_done_email in en-US only): ensure answered 201 then 200 with com.example.showcase.custom; a declared draft of en-US in the pair saved 200. The write side of the per-locale gate is the report's finding, not this PR's.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jTJcKuqRSJyv696zZ9

…ta area

An email template or a translation opens a simplified form editor in a
package's All metadata area: Setup's form and the type's preview, mounted.
A writable package saves its draft; a read-only base customizes the item
into its pair with N-17's controls, one email template locale at a time.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTJcKuqRSJyv696zZ9
…ion editor

The editor over a per-member server double and the real ensure door: a
writable package's draft, a read-only base's pair draft for one template
locale and one translation bundle, the other locale untouched, Restore and
the walled caller. The area routes the two types to it, and the surface
hands it the customization and its pending count.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTJcKuqRSJyv696zZ9
@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#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

席位核对 · ACCEPT · 2026-11-01T16:57Z

Head-sha: ca3cc5658795b0c138c527b2acbc4fdd735d5975

  • Shape: draft → main, first content line Fixes #28569, no other card beside a closing keyword. 11 files, +1174 / −25 (1199 lines ≤ 3000), no governed path (check-governed-merges --pr 28743: 0 of 11). Clause-②: no / Metadata-protocol: none line-initial in body and changeset, as claimed (6559946816, amended 6559983949). New strings are three engine.studio.area.formEditor.* keys in @object-ui/i18n's studio namespace (Record<string, string>, en + zh); no locales/* pack key moved. No contract review is owed. Every file is inside the amended surface.
  • Wanted, item by item, read in the diff:
    1. areaItemOpensFormEditor (metadataArea.ts) routes email_template and translation on a package leg to StudioBundleItemEditor; the cross-package leg, the environment scope and ?review=1 keep the generic item page. The editor mounts Setup's form (served schema, else the default-schemas.ts entry) and the registered preview; nothing is copied.
    2. Writable package: the draft is written through the shared Studio autosave and pin. Read-only base: Customize from N-17's StudioBaseCustomization; the first save asks the ensure door once and writes the pair's draft declaring overrides: { package, basedOn }; Restore deletes the pair's draft, then its row; a walled caller gets Customize disabled with the reason.
    3. Email template per (name, locale): ?member= reaches every read, the badge (useServedOverride now keys and reads per member), the save and both resets; the locale is hidden from the form, so an edit never moves into another member.
    4. Translation: the whole-bundle replace is said before the first save.
    5. Setup's pages are unchanged; StudioDesignSurface.tsx only passes the customization, publish nonce and draft-saved props down.
  • Gates (check-runs on this head): 44 success, 11 skipped, 0 failed or pending. mergeable_state clean.
  • Tests: editor 6 cases (StudioBundleItemEditor.customize-28569), routes +4, surface +2; the dev's ablations A1–A6 each went red and were restored; 159 files / 2064 tests green around the touched area; type-check app-shell + i18n 0; derived gates 34/34 exit 0.
  • Acceptance notes: filed metadata-protocol: the override write gate judges an email template by (type, name), so a <base>.custom pair can declare and publish an override of a locale the base does not ship #28746 (domain:server): the override write gate judges an email template by (type, name), so a pair can declare and publish an override of a locale the base does not ship (reach measured live). This PR never writes such a member.
  • Out of scope / NOT MEASURED: no live Console run by the seat; other languages fall back to en for the three new keys, as the whole studio namespace does. Skills-docs: not owed.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #28569 closes the card on merge. Seat 4's 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 (N-19) may edit StudioDesignSurface.tsx / StudioCustomizeControls.tsx; the queue rebuilds whichever lands later.

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