diff --git a/.changeset/30119-studio-customization-environment-line.md b/.changeset/30119-studio-customization-environment-line.md new file mode 100644 index 00000000000..02b055a3ba6 --- /dev/null +++ b/.changeset/30119-studio-customization-environment-line.md @@ -0,0 +1,11 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +--- + +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) + +Clause-②: yes (widening) — new `@object-ui/i18n` key `preview.changes.customizationEnvironment` in every pack; `preview/savedToCustomization` gains `customizationEnvironmentText` and `PACKAGE_EXPORT_COMMAND` +Metadata-protocol: none + +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`. diff --git a/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx b/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx index 4b7c507fb48..2dad63f0118 100644 --- a/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx +++ b/ui/packages/app-shell/src/preview/DraftChangesPanel.tsx @@ -87,7 +87,7 @@ import { // spec-named local declaration is what `scripts/check-spec-symbol-derivation.mjs` // refuses, and a faithful copy is exactly the fork that guard exists to prevent. import { fetchPendingDrafts } from './usePendingDrafts.js'; -import { savedToCustomizationText } from './savedToCustomization.js'; +import { customizationEnvironmentText, savedToCustomizationText } from './savedToCustomization.js'; import { canonicalMetaUrlType } from '@objectstack/spec/shared'; import { resolveI18nLabel, type I18nLabel } from '@objectstack/spec/ui'; import { MetadataProtectionFields, OverrideDeclarationFields, stripReadDecorations } from '@objectstack/spec/kernel'; @@ -1406,7 +1406,9 @@ export interface DraftChangesPanelProps { * objectstack ADR-0146 D3 — the label of the read-only base whose * `.custom` pair these drafts are saved to. When set, the sheet says * so under its title ({@link savedToCustomizationText}); the host passes it - * only while it reviews a pair's drafts. + * only while it reviews a pair's drafts, so `packageId` is then the pair, and + * the sheet names it in the line that says where the changes live + * ({@link customizationEnvironmentText}, objectstack#30119). */ customizationOf?: string; } @@ -1702,6 +1704,14 @@ export function DraftChangesPanel({ {savedToCustomizationText(t, customizationOf)}

) : null} + {/* objectstack#30119 (ADR-0146 D2) — and where they live: this + * environment's database, as items of the pair (`packageId`, the + * pair the host reviews), moved only by `os package export`. */} + {customizationOf && packageId ? ( +

+ {customizationEnvironmentText(t, packageId)} +

+ ) : null} {posture === 'confirm' && entries && entries.length > 0 && !error ? ( // objectui#12517 — what the publish carries, counted off the same diff --git a/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx b/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx index 4bb97c601f6..4dc35ee4f32 100644 --- a/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx +++ b/ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx @@ -27,6 +27,7 @@ vi.mock('@object-ui/i18n', async (importOriginal) => { import { createI18n, I18nProvider } from '@object-ui/i18n'; import { DraftChangesPanel, computeChangeDetail } from '../DraftChangesPanel'; +import { PACKAGE_EXPORT_COMMAND, customizationEnvironmentText } from '../savedToCustomization'; afterEach(() => { vi.restoreAllMocks(); @@ -293,5 +294,40 @@ describe('DraftChangesPanel — names the base whose customizations it saves (ob renderIn('en'); await waitFor(() => expect(screen.getByText('ticket')).toBeInTheDocument()); expect(screen.queryByTestId('draft-changes-saved-to')).not.toBeInTheDocument(); + expect(screen.queryByTestId('draft-changes-environment')).not.toBeInTheDocument(); + }); + + /* + * objectstack#30119 (ADR-0146 D2) — under it, where the changes live: this + * environment's database, as items of the pair the host reviews (its + * `packageId`, named as served), and the one command that moves them, + * verbatim in every pack. + */ + it('names the pair and `os package export` in the line under it, in en', async () => { + renderIn('en', { customizationOf: 'HotCRM', packageId: 'com.hotcrm.custom' }); + const line = await screen.findByTestId('draft-changes-environment'); + expect(line.textContent).toContain('this environment’s database'); + expect(line.textContent).toContain('com.hotcrm.custom'); + expect(line.textContent).toContain('os package export'); + }); + + it('keeps the pair id and the command untranslated in zh', async () => { + renderIn('zh', { customizationOf: 'HotCRM', packageId: 'com.hotcrm.custom' }); + const line = await screen.findByTestId('draft-changes-environment'); + expect(line.textContent).toContain('当前环境的数据库'); + expect(line.textContent).toContain('com.hotcrm.custom'); + expect(line.textContent).toContain('os package export'); + }); +}); + +describe('the environment line keeps the command verbatim in every pack (objectstack#30119)', () => { + const LOCALES = ['en', 'zh', 'ja', 'ko', 'de', 'fr', 'es', 'pt', 'ru', 'ar'] as const; + it.each(LOCALES)('%s: names the pair and `os package export` once each', async (lang) => { + const i18n = createI18n({ defaultLanguage: lang, detectBrowserLanguage: false }); + await i18n.changeLanguage(lang); + const text = customizationEnvironmentText(i18n.t.bind(i18n) as never, 'com.hotcrm.custom'); + expect(text).not.toContain('{{'); + expect(text.split('com.hotcrm.custom')).toHaveLength(2); + expect(text.split(PACKAGE_EXPORT_COMMAND)).toHaveLength(2); }); }); diff --git a/ui/packages/app-shell/src/preview/savedToCustomization.tsx b/ui/packages/app-shell/src/preview/savedToCustomization.tsx index 02484246cba..74243347c7e 100644 --- a/ui/packages/app-shell/src/preview/savedToCustomization.tsx +++ b/ui/packages/app-shell/src/preview/savedToCustomization.tsx @@ -23,6 +23,29 @@ export function savedToCustomizationText(t: Translate, baseLabel: string): strin }); } +/** + * The command that moves a customization to another environment, spelled + * verbatim in every language: it is typed, not read (objectstack#30119). + */ +export const PACKAGE_EXPORT_COMMAND = 'os package export'; + +/** + * objectstack#30119 (ADR-0146 D2) — the sentence under "Saved to BASE's + * customizations" in the pending-changes sheet: the changes live in this + * environment's database as items of `pairId`, the `.custom` package + * the server answered for the base, not in the project's source, and only + * {@link PACKAGE_EXPORT_COMMAND} takes them to another environment. True on + * every deployment, so it is not switched by the served `environment`. + */ +export function customizationEnvironmentText(t: Translate, pairId: string): string { + return t('preview.changes.customizationEnvironment', { + pair: pairId, + command: PACKAGE_EXPORT_COMMAND, + defaultValue: + '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}}.', + }); +} + /** {@link savedToCustomizationText} as an element, for the Studio item header's note line. */ export function SavedToCustomizationNote({ baseLabel, testId }: { baseLabel: string; testId?: string }) { const { t } = useObjectTranslation(); diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index dcc6355bb80..a3723e1fee6 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -4947,6 +4947,7 @@ const ar = { title: 'التغييرات المعلّقة', description: 'ما الذي سيغيّره النشر. تُضاف العناصر الجديدة؛ وتستبدل التحديثات النسخة المنشورة.', savedToCustomization: 'محفوظ في تخصيصات {{package}}', + customizationEnvironment: 'تُحفظ هذه التغييرات في قاعدة بيانات هذه البيئة كعناصر في {{pair}}، وليست في الشيفرة المصدرية لمشروعك. لا تنتقل إلى بيئة أخرى إلا عبر {{command}}.', loading: 'جارٍ تحميل التغييرات المعلّقة…', loadFailed: 'تعذّر تحميل التغييرات المعلّقة:', empty: 'لا شيء معلّق — نُشرت كل المسودات.', diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 68f65768a93..dabc1991cc1 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -4542,6 +4542,7 @@ const de = { title: 'Ausstehende Änderungen', description: 'Was die Veröffentlichung ändert. Neue Elemente werden hinzugefügt; Aktualisierungen überschreiben die Live-Version.', savedToCustomization: 'Gespeichert in den Anpassungen von {{package}}', + 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}}.', loading: 'Ausstehende Änderungen werden geladen…', loadFailed: 'Ausstehende Änderungen konnten nicht geladen werden:', empty: 'Nichts ausstehend — alle Entwürfe wurden veröffentlicht.', diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index 29f4c42137a..0f4a357d3f0 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -4003,6 +4003,9 @@ const en = { description: 'What publishing will change. New items are added; updates overwrite the live version.', // ADR-0146 D3 — where a customization of a read-only base is saved: its `.custom` pair. savedToCustomization: 'Saved to {{package}}’s customizations', + // objectstack#30119 (ADR-0146 D2) — the pair lives in this environment's database and moves only by export; + // `command` is always the literal `os package export`, never translated. + 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}}.', loading: 'Loading pending changes…', loadFailed: 'Could not load pending changes:', empty: 'Nothing pending — every draft has been published.', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index 065c8536332..5c638585aaf 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -4644,6 +4644,7 @@ const es = { title: 'Cambios pendientes', description: 'Lo que cambiará al publicar. Los elementos nuevos se añaden; las actualizaciones sobrescriben la versión publicada.', savedToCustomization: 'Guardado en las personalizaciones de {{package}}', + 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}}.', loading: 'Cargando cambios pendientes…', loadFailed: 'No se pudieron cargar los cambios pendientes:', empty: 'Nada pendiente — todos los borradores se han publicado.', diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index 0b212d63a83..d49d8c70da2 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -4640,6 +4640,7 @@ const fr = { title: 'Modifications en attente', description: 'Ce que la publication changera. Les nouveaux éléments sont ajoutés ; les mises à jour remplacent la version en ligne.', savedToCustomization: 'Enregistré dans les personnalisations de {{package}}', + 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}}.', loading: 'Chargement des modifications en attente…', loadFailed: 'Impossible de charger les modifications en attente :', empty: 'Rien en attente — tous les brouillons ont été publiés.', diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index 18616912242..7b16fb2083b 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -4544,6 +4544,7 @@ const ja = { title: '保留中の変更', description: '公開で何が変わるか。新規項目は追加され、更新は公開中のバージョンを上書きします。', savedToCustomization: '{{package}} のカスタマイズに保存', + customizationEnvironment: 'これらの変更はこの環境のデータベースに {{pair}} の項目として保存され、プロジェクトのソースには含まれません。別の環境へは {{command}} でのみ移せます。', loading: '保留中の変更を読み込み中…', loadFailed: '保留中の変更を読み込めませんでした:', empty: '保留中の変更はありません — すべてのドラフトが公開済みです。', diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 946e04c8575..6856b638771 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -4541,6 +4541,7 @@ const ko = { title: '대기 중인 변경 사항', description: '게시 시 변경되는 내용입니다. 새 항목은 추가되고 업데이트는 게시된 버전을 덮어씁니다.', savedToCustomization: '{{package}}의 사용자 지정에 저장됨', + customizationEnvironment: '이 변경 사항은 이 환경의 데이터베이스에 {{pair}}의 항목으로 저장되며 프로젝트 소스에는 없습니다. 다른 환경으로는 {{command}}로만 옮길 수 있습니다.', loading: '대기 중인 변경 사항 불러오는 중…', loadFailed: '대기 중인 변경 사항을 불러오지 못했습니다:', empty: '대기 중인 항목 없음 — 모든 드래프트가 게시되었습니다.', diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index d5aa95ee3df..c3cf0d8412e 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -4639,6 +4639,7 @@ const pt = { title: 'Alterações pendentes', description: 'O que a publicação vai mudar. Itens novos são adicionados; atualizações sobrescrevem a versão publicada.', savedToCustomization: 'Salvo nas personalizações de {{package}}', + customizationEnvironment: 'Estas alterações ficam no banco de dados deste ambiente, como itens de {{pair}}, e não no código-fonte do seu projeto. Só chegam a outro ambiente por meio de {{command}}.', loading: 'Carregando alterações pendentes…', loadFailed: 'Não foi possível carregar as alterações pendentes:', empty: 'Nada pendente — todos os rascunhos foram publicados.', diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index 2e07937b6e4..18f645bce20 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -4755,6 +4755,7 @@ const ru = { title: 'Ожидающие изменения', description: 'Что изменит публикация. Новые элементы добавляются; обновления перезаписывают опубликованную версию.', savedToCustomization: 'Сохранено в настройках {{package}}', + customizationEnvironment: 'Эти изменения хранятся в базе данных этой среды как элементы {{pair}}, а не в исходном коде проекта. В другую среду они попадают только через {{command}}.', loading: 'Загрузка ожидающих изменений…', loadFailed: 'Не удалось загрузить ожидающие изменения:', empty: 'Ничего не ожидает — все черновики опубликованы.', diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index 2a236694f41..75c5e9c4847 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -3463,6 +3463,7 @@ const zh = { title: '待发布的变更', description: '发布将带来以下变更。“新增”为全新内容,“更新”将覆盖线上版本。', savedToCustomization: '保存到 {{package}} 的定制', + customizationEnvironment: '这些变更保存在当前环境的数据库里,是 {{pair}} 的条目,不在项目源码中;只有通过 {{command}} 才能带到其他环境。', loading: '正在加载待发布变更…', loadFailed: '无法加载待发布变更:', empty: '没有待发布的内容 —— 所有草稿均已发布。',