Skip to content

Studio: email template and translation editors in the All metadata area — a read-only base's template or bundle is customized as an override in <base>.custom #28569

Closed

Description

@objectstack-fleet

epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848

Filing gate: ④ child of epic #28236 (P2, cut 2026-11-01 on the maintainer's 「四项提速措施全部按推荐执行」, schedule reviewed) · plan of record: ADR-0146 Rollout P2 "N-25 email template and translation editors into Studio", and the last paragraph of its Consequences. ⛔ Not a claim.

Blocked-by: #28410, #28407, #28562, #28561

Part of #28236 (P2, child N-25) · governs: ADR-0146 Consequences (last paragraph: "their simplified form editor is offered inside Studio's All-metadata area"), D1 (no Setup designer), D2 (the override lives in .custom) · lane: domain:ui · size: M.

Business reason. Today an admin who changes one line of HotCRM's welcome email, or one translated label, does it in Setup. That writes a package-less row which never exports, and which N-16b will refuse. With this card they edit it in Studio's All metadata area, and the change is an override in hotcrm.custom.

Measured (35b178bd0a).

  • Setup edits both types on the generic item page ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx, which does the overlay save and the layered.resettable Reset. It uses:
    • forms from default-schemas.ts (translation :118, email_template :302);
    • previews/EmailTemplatePreview.tsx (sandboxed HTML with variables) and previews/TranslationPreview.tsx (read-only coverage), registered at previews/index.ts:60–61.
  • email_template is a bundle type keyed by locale (default-schemas.ts:364). metadata-protocol/src/served-override.ts#isServedOverrideRowOf reads no bundle member. Whether N-10's read and N-09's gate key an override by (name, locale): not verified.
  • Studio has no host for either type. The Interfaces off-nav types are page, dashboard, report and view (studio-design/StudioDesignSurface.tsx:2981). The All metadata area is N-26a.
  • Both types are overridable in packages/spec/src/kernel/customization-package.ts#OVERRIDE_TYPE_CLASSIFICATION.

Wanted.

  1. In Studio's All metadata area (N-26a), an email template or a translation opens in a simplified form editor. That editor is the fields Setup's form offers today plus the existing preview, reused: one component, not a copy.
  2. On a writable package, the editor writes that package's draft. On a read-only base, Customize takes N-17's path: ensure, a draft in the pair with overrides: { package, basedOn }, Publish on the pair, and Restore package original.
  3. An email template is edited per locale, and its override is per (name, locale). If the server serves or gates a per-locale override wrongly, the dev stops and reports a server gap, with no client workaround.
  4. A translation's override is its whole locale bundle (whole replace, D2). The editor says so before the first save.
  5. Setup's pages stay until N-28. This card removes nothing there.

Tests.

  • Writable package: an edit writes that package's draft.
  • Read-only base: Customize and an edit write a pair draft carrying the declaration, for one email template locale and one translation bundle.
  • The other locale of the same template is untouched.
  • Restore deletes the pair's item and the base reads back.
  • A walled tenant admin sees Customize disabled.

Release safety. Safe on its own: it writes only through N-13 (#28407) and N-09 (#28410), and Setup keeps its form. N-16b (closing the package-less write) waits on this card, so admins always have a door for these two types.

Not here. The directory, lists and generic item page (N-26a). Directory marks (N-26b). Setup redirects (N-28). Removing Studio's overlay link (N-16b). View, dashboard and report (N-17).

Dedupe. #26649 and #27036 (closed: Setup opens each template locale as its own member). Searched email template editor studio, translation editor studio: no open card.

Links back: #28236, #28407, #28410, #26649, ADR-0146 D1, D2, Consequences.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    priority:p1High: required for production / M2
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Unblocked · 2026-11-01T15:21Z · pm:blocked → pm:queue


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Claim: PM loop round 2 · 2026-11-01T15:24Z
    Session: session_01QsR2jTJcKuqRSJyv696zZ9 (card thread, os-dev); dispatched by seat session session_01QsR2jUJ19MstFvFhjgiYTu
    Account: zhuangjianguo
    Branch: claude/project-thread-mtqfy5 (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-28569
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: the Studio All metadata area's item route in ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, a simplified email_template / translation editor reusing views/metadata-admin/default-schemas.ts forms and previews/EmailTemplatePreview.tsx + previews/TranslationPreview.tsx (one component, not a copy), studio-design/StudioCustomizeControls.tsx and studio-design/packages-io.ts (reuse; edit only where needed), their tests, studio i18n strings, and a root .changeset (stop on breach; explain in the report). Setup's pages are untouched.
    Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: 6559905672
    Premise: holds (at origin/main d62d9737, Studio has no editor for either type: the All metadata area links an item to Setup's /apps/setup/metadata/<type>/<name> page, StudioDesignSurface.tsx:913; N-17's Customize controls are on main)
    Serial constraints cleared: seat 4's #28572 (N-19, a base and its .custom as one surface), claimed now, may edit StudioDesignSurface.tsx / StudioCustomizeControls.tsx; whichever lands later merges main.

    The Clause-②: no line holds for strings in the studio namespace (Record<string, string>). A new locales/* pack key makes it yes (widening); the report says so before pr_create and the seat amends this line. Per the card: if the server serves or gates a per-(name, locale) email-template override wrongly, stop and report a server gap, with no client workaround.

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Claim amended · 2026-11-01T15:28Z · file surface widened and the premise line corrected; it adds to claim 6559946816. Branch, session, size and declaration lines are unchanged.

    • Premise, corrected: the All metadata area's item route is AreaItemOpen in studio-design/StudioMetadataArea.tsx; StudioDesignSurface.tsx:913 is StudioReadOnlyLink, the read-only chip. The premise itself holds: Studio has no editor for email_template or translation at d62d9737.
    • File surface (added): ui/packages/app-shell/src/views/studio-design/StudioMetadataArea.tsx (route both types on a package leg to the editor; take the surface's customization and draft-saved/publish props), one new studio-design/StudioBundleItemEditor.tsx (the simplified editor: the served schema, else the default-schemas.ts entry, plus the registered preview, mounted not copied), and studio-design/metadataArea.ts (the pure routing predicate). StudioDesignSurface.tsx only passes the customization props down.
    • Server reading (card rule, no stop): the served read, layered read, list merge, draft, reset and publish are all keyed per (name, locale) member. The write gate judges shipping by (type, name) only; the editor never writes an unshipped member, so the dev reports that as an out-of-scope note for this seat to weigh.
  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jTJcKuqRSJyv696zZ9
    os-dev-report

    {
      "issue": 28569,
      "status": "done",
      "branch": "claude/project-thread-mtqfy5",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/28743",
      "head": "ca3cc5658795b0c138c527b2acbc4fdd735d5975",
      "session": "session_01QsR2jTJcKuqRSJyv696zZ9",
      "premise_still_valid": true,
      "claim_lines": [
        "Clause-②: no",
        "Metadata-protocol: none"
      ],
      "measured": "Before code: the server's per-member reads hold. findServedOverlayRow narrows by overlayMemberWhere and isServedOverrideRowOf, and the layered read passes memberOption(member), so a pair row of one email template locale is served for that locale alone; the card's stop rule did not fire. The item route is AreaItemOpen in StudioMetadataArea.tsx (the seat amended the claim for it). Clause-② on source: three keys in the studio namespace table (Record<string,string>, outside the locales packs), so 'no' holds. Live on objectstack dev --fresh (examples/app-showcase, base com.example.showcase 0.1.0): ensure 201 created:true then 200 created:false, pair com.example.showcase.custom; a declared en-US draft in the pair saved 200; a draft PUT rejects ?member= (400 VALIDATION_ERROR, supported force, mode, package, surface), so the member is the body's locale.",
      "summary": "Studio's All metadata area opens an email_template or a translation of one package in a simplified editor (StudioBundleItemEditor.tsx, new): Setup's form fields (served schema, else the default schema) beside the type's registered preview (EmailTemplatePreview / TranslationPreview, mounted, not copied). areaItemOpensFormEditor routes it; ~all, ?scope=environment and ?review=1 keep the generic page. A writable package saves its draft through useDraftAutoSave and the shared version-pin guard. A read-only base takes N-17's Customize: the first saved edit calls the surface's ensure() once and saves into the pair with overrides {package, basedOn}; the badge reads the served declaration; Restore package original deletes the pair's draft then row. An email template is one item per (name, locale): reads, saves, badge and Restore carry ?member=<locale>, the locale is hidden in the form, and the other locales are untouched; a note before the first save says only that locale is customized. A translation's override is the whole bundle, and the editor says so before the first save. useServedOverride takes an optional member. StudioDesignSurface only passes customization, publishNonce and onDraftSaved. Setup's pages are unchanged.",
      "tests": "New StudioBundleItemEditor.customize-28569 (6): writable draft; read-only Customize of one template locale (ensure once, pair draft exact with declaration, badge, base untouched); the other locale untouched; Restore deletes the pair draft and row of that locale and the base reads back; walled admin sees Customize disabled; translation whole-bundle note and whole bundle saved. StudioMetadataArea.routes-28561 +4 (20 total), StudioDesignSurface.metadataArea-28561 +2. Ablations A1-A6 via scripts/ablation-replace.mjs, each landed, went red (4, 3, 1, 2, 1 failed in the editor file; 2 failed in the routes file) and was restored to HEAD by the tool. Wider set under the lock: 159 files (every test naming the touched modules plus every studio-namespace importer, zh-base-term and count-family census included), 2064 tests passed at 37edd173. turbo type-check app-shell + i18n exit 0 at 1f6a135c. ESLint on 10 changed files: 0 errors (one react-hooks/static-components error fixed with the comment EmbeddedItemEditor uses).",
      "gates": [
        "derived set at ca3cc565, reconciled with dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN",
        "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
        "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
        "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
        "node scripts/check-closing-keyword-parity.mjs :: exit 0",
        "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
        "node scripts/check-comment-mask-corpus.mjs :: exit 0",
        "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
        "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
        "node scripts/check-sdui-manifest.mjs :: exit 0",
        "node scripts/check-sdui-manifest.mjs --self-test :: exit 0",
        "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
        "node scripts/release-pending-publish.mjs --self-test :: exit 0",
        "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
        "pnpm check:changeset-gate-self-tests :: exit 0",
        "pnpm check:closed-ceremony-text :: exit 0",
        "pnpm check:cross-package-test-inputs :: exit 0",
        "pnpm check:dispatcher-error-vocabulary :: exit 0",
        "pnpm check:driver-memory-census :: exit 0",
        "pnpm check:engine-double-contract :: exit 0",
        "pnpm check:gitlink-declared :: exit 0",
        "pnpm check:nul-bytes :: exit 0",
        "pnpm check:pm-changeset-deadline-census :: exit 0",
        "pnpm check:published-files :: exit 0",
        "pnpm check:query-options-erasure :: exit 0",
        "pnpm check:refd-timer-probe :: exit 0",
        "pnpm check:skill-identifier-liveness :: exit 0",
        "pnpm check:test-source-alias :: exit 0",
        "pnpm check:tier-file-adoption :: exit 0",
        "pnpm check:type-check-coverage :: exit 0",
        "pnpm check:type-check-debt :: exit 0",
        "pnpm check:undeclared-dep-imports :: exit 0",
        "pnpm check:watch-hint-literal :: exit 0",
        "pnpm check:where-matcher :: exit 0",
        "pnpm --dir ui exec vitest run scripts/ :: exit 0",
        "PR-body gates on the sent body: check-changeset-no-major (GITHUB_EVENT_PATH) 0, check-adr-0087-registration 0, check-closing-keyword-parity 0 (--body binds only #28569), check-partof-closing-keyword (PR_BODY) 0",
        "check-sdui-manifest 0",
        "CI check-runs on #28743: not yet reported at report time"
      ],
      "dogfood_battery": "n/a: no ! in the title and no (narrowing) arm",
      "line_budget": "11 files, +1175 / -25 vs the merge base (dispatch-gates: 1199 changed lines), under the 3000-line Tier H threshold",
      "files_changed": [
        ".changeset/28569-studio-email-template-translation-editor.md",
        "ui/packages/app-shell/src/views/studio-design/StudioBundleItemEditor.tsx (new)",
        "ui/packages/app-shell/src/views/studio-design/StudioBundleItemEditor.customize-28569.test.tsx (new)",
        "ui/packages/app-shell/src/views/studio-design/{StudioCustomizeControls,StudioDesignSurface,StudioMetadataArea}.tsx",
        "ui/packages/app-shell/src/views/studio-design/metadataArea.ts",
        "ui/packages/app-shell/src/views/studio-design/{StudioMetadataArea.routes-28561,StudioDesignSurface.metadataArea-28561}.test.tsx",
        "ui/packages/i18n/src/namespaces/studio/{en,zh}.ts"
      ],
      "deviations": [
        "Two existing route/surface tests used a translation as 'a type no pillar designs'; that case now opens the editor, so they were repointed to agent/support_bot and agent/a1.",
        "Restore does not refresh the header's pending count at once; it emits the metadata-refresh pulse, as the Interfaces pillar's Restore does (in Acceptance notes).",
        "Merged origin/main once (3 commits, none in ui); the merge commit carries the two trailers.",
        "PR opened without assignees or labels; no GitHub MCP write."
      ],
      "mcp_calls": "2 reads: issue_read on #28569 (card) and pull_request_read get on #28711 (body shape). No MCP write. Separately, subscribe_pr_activity on #28743 (claude-code-remote).",
      "api_writes": "2: relay repository_dispatch x2 (pr_create #28743, run 41327284775, read-back identical 8587 bytes; this os-dev-report comment). git push x2 is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: b · Seam: spec:overrides {package, basedOn} (ADR-0146 D2) → runtime:overrideWriteVerdict / judgeOverrideWrite · reach: measured once on objectstack dev --fresh (examples/app-showcase, base com.example.showcase 0.1.0 ships email_template showcase_task_done_email in en-US only): PUT /api/v1/meta/email_template/showcase_task_done_email?mode=draft&package=com.example.showcase.custom with body locale fr-FR and overrides {package: com.example.showcase, basedOn: 0.1.0} answered 200 (seq 2), the pair's draft at member fr-FR read back carrying the declaration, and POST .../publish?package=com.example.showcase.custom&member=fr-FR answered 200 (seq 3): a declared override of a locale the base does not ship is admitted and goes live. (?member= on the PUT itself is refused 400 as unsupported; the member is the body's locale.) · evidence: packages/metadata-protocol/src/protocol-packaged-base-refusal.ts:459 shippers = shippedArtifactsOf(request.type, request.name), not per member; :339-340 declared_package_ships_no_such_item checks only that list; OverrideWriteSubject (:202) carries no member · the editor never writes such a row (it saves the locale it read) · dedupe words: override unshipped locale, declared_package_ships_no_such_item member, email_template override per locale gate"
      ]
    }
  6. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Landed · 2026-11-01T17:15Z · PR #28743 merged through the merge queue as 272fec05 at 2026-11-01T17:14:23Z (GitHub's clock). ACCEPT 6561010583 at head ca3cc565.

    probe before (272fec05~1) now (origin/main)
    ui/packages/app-shell/src/views/studio-design/StudioBundleItemEditor.tsx exists no yes
    areaItemOpensFormEditor in studio-design/metadataArea.ts 0 1

    Fixes #28569 closed this card; pm:dispatched comes off in the same stroke as this note. Follow-up filed from this card: #28746 (domain:server). Unblocks: #28578 (N-28), whose last open blocker this was.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uienhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions