Skip to content

Commit b6caa14

Browse files
fix(app-shell): Studio's publish review sheet says a customization lives in this environment's database and moves only by os package export (#30435)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgJoNqzsoiqe3Hb6Lgi2XQ2n)_ domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG ## 维护者速读 - 改了什么:Studio 发布前的「待发布的变更」面板,在「保存到 X 的定制」下面加一行:这些变更保存在当前环境的数据库里,是 `BASE.custom` 的条目(写的是服务端给的真实包 id),不在项目源码中;只有通过 `os package export` 才能带到其他环境(十种语言,命令原样不翻译)。 - 为什么改:开发者在 `os dev` 的 Studio 里调好视图,`os build` 部署后改动不见了,Studio 从没说过它只在这个库里(ADR-0146 D2)。CLI 和脚手架那一半已由 PR #30336 落地,这是 Studio 这一半。 - 风险与回滚:只在面板审阅只读包的定制(host 传了 customizationOf)时多一行文字,其他面板和可写包一字不变;回滚即撤本 PR。 - 先看:ui/packages/app-shell/src/preview/savedToCustomization.tsx 的 customizationEnvironmentText;ui/packages/app-shell/src/preview/DraftChangesPanel.tsx 的 draft-changes-environment 那一行;ui/packages/i18n/src/locales/zh.ts 的 customizationEnvironment。 - 两分钟验证:examples/app-showcase 跑 `objectstack dev --seed-admin --fresh`,在 Studio 定制一个 Showcase 视图后点「变更」,面板标题下应看到「保存到 ObjectStack Showcase 的定制」和新的一行,点名 com.example.showcase.custom 与 os package export。 - 需要你做的:无,席位落地。 Fixes #30119 Clause-②: yes (widening) Metadata-protocol: none ## What changes The card's first Expected bullet: "Studio says plainly, where a customization is saved, that it lives in this environment's database and how it moves." Triage 6652867871 states the done line: a Studio notice that a customization lives in this environment as an item of `BASE.custom` and travels only by `os package export`. The server half (CLI `os build` notice, scaffold README and AGENTS.md) landed in PR #30336. - **Where.** The one sentence that already names where a customization is saved, "Saved to BASE's customizations" (`preview/savedToCustomization.tsx`, ADR-0146 D3), is drawn in two places: the pending-changes sheet (`DraftChangesPanel`) and the Studio item header's note line. The new line sits under it in the **sheet**, once per surface, not on a save toast. The sheet is where a person reviews the pair's drafts before they go live, and every pillar opens the same sheet. - **The real package id.** The host passes `customizationOf` only while it reviews a pair's drafts, and then its `packageId` is the pair: the ensure door's answer or the listed pair the server serves (`StudioDesignSurface`'s `draftScope`). The sheet names that id. It computes no `.custom` spelling. - **The command.** `os package export` is the constant `PACKAGE_EXPORT_COMMAND`, passed into every pack as `{{command}}`, so no translation can change it. - **`@object-ui/i18n`:** `preview.changes.customizationEnvironment` in all ten packs (`{{pair}}`, `{{command}}`). - **Not switched by environment.** The console can tell: `/api/v1/discovery` serves `environment` (`production` / `sandbox` / `development`, from `NODE_ENV`; measured `"development"` under `objectstack dev`). The line is shown everywhere anyway, because it is true everywhere: a customization made in production also lives only in that environment's database and reaches staging or a new environment only by export. Gating it on `development` would hide a true statement where it still applies. ## Tests Head `8fc8d012` (after one merge of `origin/main`, which brought PR #30361's change to the same file in cleanly). - `DraftChangesPanel.test.tsx`, in the existing "names the base whose customizations it saves" describe, through the real i18next hook and shipped packs: the line names `this environment's database`, `com.hotcrm.custom` and `os package export` in en, keeps the pair id and the command untranslated in zh, and the control (a writable package's own drafts) draws neither sentence. A new `it.each` over all ten packs: `customizationEnvironmentText` names the pair once and `PACKAGE_EXPORT_COMMAND` once, with no `{{` left. - Ablation, one leg: the render condition mutated to `false && …` through `scripts/ablation-replace.mjs` (anchor 1 → 0, blob changed): exactly the two new render tests fail, 26 pass; restored, blob identical to `HEAD`. <details><summary>Local runs</summary> - `vitest run` (from `ui/`, under the verify lock): `DraftChangesPanel.test.tsx`, `StudioDesignSurface.customizePublish-28562.test.tsx` (the importer that pins the shared sentence) and the whole `packages/i18n/` (key parity included): Test Files 97 passed (97), Tests 1479 passed, 13 skipped. VERDICT command-exit 0. - `turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n --concurrency=2`: 69 tasks successful, VERDICT command-exit 0 (on the merged head). - `pnpm check:type-check-debt`: exit 0. </details> ## Acceptance notes - The Studio item header's note line also says "Saved to BASE's customizations" (four call sites, all in `StudioDesignSurface.tsx`, which PR #30381 holds). It passes only the base's label, so naming the pair there needs that file. Left out; the seat files it as a follow-up after #30381. - Surface widened by the seat (thread ruling 10:22Z): `preview/DraftChangesPanel.tsx`, `preview/savedToCustomization.tsx` and their test, outside the Claim's `studio-design/**`. ## Verification Live walk: `examples/app-showcase`, `objectstack dev --seed-admin --fresh -p 4730`, console on Vite against it, signed in as the seeded admin. `POST /api/v1/packages/com.example.showcase/ensure-customization` answered 201 with `packageId: com.example.showcase.custom`; a draft of view `showcase_task.in_progress` was saved into the pair with `overrides: { package: com.example.showcase, basedOn: 0.1.0 }`; then `/studio/com.example.showcase/interfaces?changes=review`. | Locale | Before (sources at `d3f23d20`) | After (this branch) | |:--|:--|:--| | zh-CN | 保存到 ObjectStack Showcase 的定制 (nothing else) | 保存到 ObjectStack Showcase 的定制 · 这些变更保存在当前环境的数据库里,是 com.example.showcase.custom 的条目,不在项目源码中;只有通过 os package export 才能带到其他环境。 | | en | Saved to ObjectStack Showcase’s customizations (nothing else) | Saved to ObjectStack Showcase’s customizations · These changes are kept in this environment’s database as items of com.example.showcase.custom, not in your project’s source. They reach another environment only through os package export. | The before leg is the same boot and the same draft, with the two `preview/` files checked out at `d3f23d20` (Vite reloaded them; restored afterwards, `git diff HEAD` empty). Screenshots (`before-zh-CN.png`, `before-en.png`, `after-zh-CN.png`, `after-en.png`) are in the project files under `ui-seat3/30119/`. <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `8fc8d012`: 35 families, each run with its exit code captured before any pipe, all exit 0 (`node scripts/check-sdui-manifest.mjs` among them; `pnpm check:type-check-debt` under the verify lock). `--ran`: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. `pnpm --dir ui check:new-line-citations`: 0 new citations. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6Cay8ZjjPDJ1vkTcVDd)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2b35374 commit b6caa14

14 files changed

Lines changed: 94 additions & 2 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(app-shell): Studio's publish review sheet says a customization lives in this environment's database and moves only by `os package export` (#30119)
7+
8+
Clause-②: yes (widening) — new `@object-ui/i18n` key `preview.changes.customizationEnvironment` in every pack; `preview/savedToCustomization` gains `customizationEnvironmentText` and `PACKAGE_EXPORT_COMMAND`
9+
Metadata-protocol: none
10+
11+
Reviewing a read-only package's customizations, the sheet adds one line under "Saved to BASE's customizations": the changes are kept in this environment's database as items of the `.custom` package it names (the id the server answered for the base), not in the project's source, and they reach another environment only through `os package export`.

‎ui/packages/app-shell/src/preview/DraftChangesPanel.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ import {
8787
// spec-named local declaration is what `scripts/check-spec-symbol-derivation.mjs`
8888
// refuses, and a faithful copy is exactly the fork that guard exists to prevent.
8989
import { fetchPendingDrafts } from './usePendingDrafts.js';
90-
import { savedToCustomizationText } from './savedToCustomization.js';
90+
import { customizationEnvironmentText, savedToCustomizationText } from './savedToCustomization.js';
9191
import { canonicalMetaUrlType } from '@objectstack/spec/shared';
9292
import { resolveI18nLabel, type I18nLabel } from '@objectstack/spec/ui';
9393
import { MetadataProtectionFields, OverrideDeclarationFields, stripReadDecorations } from '@objectstack/spec/kernel';
@@ -1406,7 +1406,9 @@ export interface DraftChangesPanelProps {
14061406
* objectstack ADR-0146 D3 — the label of the read-only base whose
14071407
* `<base>.custom` pair these drafts are saved to. When set, the sheet says
14081408
* so under its title ({@link savedToCustomizationText}); the host passes it
1409-
* only while it reviews a pair's drafts.
1409+
* only while it reviews a pair's drafts, so `packageId` is then the pair, and
1410+
* the sheet names it in the line that says where the changes live
1411+
* ({@link customizationEnvironmentText}, objectstack#30119).
14101412
*/
14111413
customizationOf?: string;
14121414
}
@@ -1702,6 +1704,14 @@ export function DraftChangesPanel({
17021704
{savedToCustomizationText(t, customizationOf)}
17031705
</p>
17041706
) : null}
1707+
{/* objectstack#30119 (ADR-0146 D2) — and where they live: this
1708+
* environment's database, as items of the pair (`packageId`, the
1709+
* pair the host reviews), moved only by `os package export`. */}
1710+
{customizationOf && packageId ? (
1711+
<p data-testid="draft-changes-environment" className="text-xs text-muted-foreground">
1712+
{customizationEnvironmentText(t, packageId)}
1713+
</p>
1714+
) : null}
17051715
</SheetHeader>
17061716
{posture === 'confirm' && entries && entries.length > 0 && !error ? (
17071717
// objectui#12517 — what the publish carries, counted off the same

‎ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ vi.mock('@object-ui/i18n', async (importOriginal) => {
2727

2828
import { createI18n, I18nProvider } from '@object-ui/i18n';
2929
import { DraftChangesPanel, computeChangeDetail } from '../DraftChangesPanel';
30+
import { PACKAGE_EXPORT_COMMAND, customizationEnvironmentText } from '../savedToCustomization';
3031

3132
afterEach(() => {
3233
vi.restoreAllMocks();
@@ -293,5 +294,40 @@ describe('DraftChangesPanel — names the base whose customizations it saves (ob
293294
renderIn('en');
294295
await waitFor(() => expect(screen.getByText('ticket')).toBeInTheDocument());
295296
expect(screen.queryByTestId('draft-changes-saved-to')).not.toBeInTheDocument();
297+
expect(screen.queryByTestId('draft-changes-environment')).not.toBeInTheDocument();
298+
});
299+
300+
/*
301+
* objectstack#30119 (ADR-0146 D2) — under it, where the changes live: this
302+
* environment's database, as items of the pair the host reviews (its
303+
* `packageId`, named as served), and the one command that moves them,
304+
* verbatim in every pack.
305+
*/
306+
it('names the pair and `os package export` in the line under it, in en', async () => {
307+
renderIn('en', { customizationOf: 'HotCRM', packageId: 'com.hotcrm.custom' });
308+
const line = await screen.findByTestId('draft-changes-environment');
309+
expect(line.textContent).toContain('this environment’s database');
310+
expect(line.textContent).toContain('com.hotcrm.custom');
311+
expect(line.textContent).toContain('os package export');
312+
});
313+
314+
it('keeps the pair id and the command untranslated in zh', async () => {
315+
renderIn('zh', { customizationOf: 'HotCRM', packageId: 'com.hotcrm.custom' });
316+
const line = await screen.findByTestId('draft-changes-environment');
317+
expect(line.textContent).toContain('当前环境的数据库');
318+
expect(line.textContent).toContain('com.hotcrm.custom');
319+
expect(line.textContent).toContain('os package export');
320+
});
321+
});
322+
323+
describe('the environment line keeps the command verbatim in every pack (objectstack#30119)', () => {
324+
const LOCALES = ['en', 'zh', 'ja', 'ko', 'de', 'fr', 'es', 'pt', 'ru', 'ar'] as const;
325+
it.each(LOCALES)('%s: names the pair and `os package export` once each', async (lang) => {
326+
const i18n = createI18n({ defaultLanguage: lang, detectBrowserLanguage: false });
327+
await i18n.changeLanguage(lang);
328+
const text = customizationEnvironmentText(i18n.t.bind(i18n) as never, 'com.hotcrm.custom');
329+
expect(text).not.toContain('{{');
330+
expect(text.split('com.hotcrm.custom')).toHaveLength(2);
331+
expect(text.split(PACKAGE_EXPORT_COMMAND)).toHaveLength(2);
296332
});
297333
});

‎ui/packages/app-shell/src/preview/savedToCustomization.tsx‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,29 @@ export function savedToCustomizationText(t: Translate, baseLabel: string): strin
2323
});
2424
}
2525

26+
/**
27+
* The command that moves a customization to another environment, spelled
28+
* verbatim in every language: it is typed, not read (objectstack#30119).
29+
*/
30+
export const PACKAGE_EXPORT_COMMAND = 'os package export';
31+
32+
/**
33+
* objectstack#30119 (ADR-0146 D2) — the sentence under "Saved to BASE's
34+
* customizations" in the pending-changes sheet: the changes live in this
35+
* environment's database as items of `pairId`, the `<base>.custom` package
36+
* the server answered for the base, not in the project's source, and only
37+
* {@link PACKAGE_EXPORT_COMMAND} takes them to another environment. True on
38+
* every deployment, so it is not switched by the served `environment`.
39+
*/
40+
export function customizationEnvironmentText(t: Translate, pairId: string): string {
41+
return t('preview.changes.customizationEnvironment', {
42+
pair: pairId,
43+
command: PACKAGE_EXPORT_COMMAND,
44+
defaultValue:
45+
'These changes are kept in this environment’s database as items of {{pair}}, not in your project’s source. They reach another environment only through {{command}}.',
46+
});
47+
}
48+
2649
/** {@link savedToCustomizationText} as an element, for the Studio item header's note line. */
2750
export function SavedToCustomizationNote({ baseLabel, testId }: { baseLabel: string; testId?: string }) {
2851
const { t } = useObjectTranslation();

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4951,6 +4951,7 @@ const ar = {
49514951
title: 'التغييرات المعلّقة',
49524952
description: 'ما الذي سيغيّره النشر. تُضاف العناصر الجديدة؛ وتستبدل التحديثات النسخة المنشورة.',
49534953
savedToCustomization: 'محفوظ في تخصيصات {{package}}',
4954+
customizationEnvironment: 'تُحفظ هذه التغييرات في قاعدة بيانات هذه البيئة كعناصر في {{pair}}، وليست في الشيفرة المصدرية لمشروعك. لا تنتقل إلى بيئة أخرى إلا عبر {{command}}.',
49544955
loading: 'جارٍ تحميل التغييرات المعلّقة…',
49554956
loadFailed: 'تعذّر تحميل التغييرات المعلّقة:',
49564957
empty: 'لا شيء معلّق — نُشرت كل المسودات.',

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4546,6 +4546,7 @@ const de = {
45464546
title: 'Ausstehende Änderungen',
45474547
description: 'Was die Veröffentlichung ändert. Neue Elemente werden hinzugefügt; Aktualisierungen überschreiben die Live-Version.',
45484548
savedToCustomization: 'Gespeichert in den Anpassungen von {{package}}',
4549+
customizationEnvironment: 'Diese Änderungen liegen in der Datenbank dieser Umgebung, als Elemente von {{pair}}, nicht im Quellcode Ihres Projekts. In eine andere Umgebung gelangen sie nur mit {{command}}.',
45494550
loading: 'Ausstehende Änderungen werden geladen…',
45504551
loadFailed: 'Ausstehende Änderungen konnten nicht geladen werden:',
45514552
empty: 'Nichts ausstehend — alle Entwürfe wurden veröffentlicht.',

‎ui/packages/i18n/src/locales/en.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4009,6 +4009,9 @@ const en = {
40094009
description: 'What publishing will change. New items are added; updates overwrite the live version.',
40104010
// ADR-0146 D3 — where a customization of a read-only base is saved: its `.custom` pair.
40114011
savedToCustomization: 'Saved to {{package}}’s customizations',
4012+
// objectstack#30119 (ADR-0146 D2) — the pair lives in this environment's database and moves only by export;
4013+
// `command` is always the literal `os package export`, never translated.
4014+
customizationEnvironment: 'These changes are kept in this environment’s database as items of {{pair}}, not in your project’s source. They reach another environment only through {{command}}.',
40124015
loading: 'Loading pending changes…',
40134016
loadFailed: 'Could not load pending changes:',
40144017
empty: 'Nothing pending — every draft has been published.',

‎ui/packages/i18n/src/locales/es.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4648,6 +4648,7 @@ const es = {
46484648
title: 'Cambios pendientes',
46494649
description: 'Lo que cambiará al publicar. Los elementos nuevos se añaden; las actualizaciones sobrescriben la versión publicada.',
46504650
savedToCustomization: 'Guardado en las personalizaciones de {{package}}',
4651+
customizationEnvironment: 'Estos cambios se guardan en la base de datos de este entorno, como elementos de {{pair}}, no en el código fuente de su proyecto. Solo llegan a otro entorno mediante {{command}}.',
46514652
loading: 'Cargando cambios pendientes…',
46524653
loadFailed: 'No se pudieron cargar los cambios pendientes:',
46534654
empty: 'Nada pendiente — todos los borradores se han publicado.',

‎ui/packages/i18n/src/locales/fr.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4644,6 +4644,7 @@ const fr = {
46444644
title: 'Modifications en attente',
46454645
description: 'Ce que la publication changera. Les nouveaux éléments sont ajoutés ; les mises à jour remplacent la version en ligne.',
46464646
savedToCustomization: 'Enregistré dans les personnalisations de {{package}}',
4647+
customizationEnvironment: 'Ces modifications sont stockées dans la base de données de cet environnement, comme éléments de {{pair}}, et non dans les sources de votre projet. Elles ne passent dans un autre environnement que par {{command}}.',
46474648
loading: 'Chargement des modifications en attente…',
46484649
loadFailed: 'Impossible de charger les modifications en attente :',
46494650
empty: 'Rien en attente — tous les brouillons ont été publiés.',

‎ui/packages/i18n/src/locales/ja.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4548,6 +4548,7 @@ const ja = {
45484548
title: '保留中の変更',
45494549
description: '公開で何が変わるか。新規項目は追加され、更新は公開中のバージョンを上書きします。',
45504550
savedToCustomization: '{{package}} のカスタマイズに保存',
4551+
customizationEnvironment: 'これらの変更はこの環境のデータベースに {{pair}} の項目として保存され、プロジェクトのソースには含まれません。別の環境へは {{command}} でのみ移せます。',
45514552
loading: '保留中の変更を読み込み中…',
45524553
loadFailed: '保留中の変更を読み込めませんでした:',
45534554
empty: '保留中の変更はありません — すべてのドラフトが公開済みです。',

0 commit comments

Comments
 (0)