From 6e757974b20117133557c837b163985b7eadb266 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 10:28:28 +0000 Subject: [PATCH 1/2] feat(app-shell): Studio's publish review sheet says a customization lives in this environment's database and moves only by os package export Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Utp6Cay8ZjjPDJ1vkTcVDd --- ...9-studio-customization-environment-line.md | 11 ++++++ .../src/preview/DraftChangesPanel.tsx | 14 ++++++-- .../__tests__/DraftChangesPanel.test.tsx | 36 +++++++++++++++++++ .../src/preview/savedToCustomization.tsx | 23 ++++++++++++ ui/packages/i18n/src/locales/ar.ts | 1 + ui/packages/i18n/src/locales/de.ts | 1 + ui/packages/i18n/src/locales/en.ts | 3 ++ ui/packages/i18n/src/locales/es.ts | 1 + ui/packages/i18n/src/locales/fr.ts | 1 + ui/packages/i18n/src/locales/ja.ts | 1 + ui/packages/i18n/src/locales/ko.ts | 1 + ui/packages/i18n/src/locales/pt.ts | 1 + ui/packages/i18n/src/locales/ru.ts | 1 + ui/packages/i18n/src/locales/zh.ts | 1 + 14 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 .changeset/30119-studio-customization-environment-line.md diff --git a/.changeset/30119-studio-customization-environment-line.md b/.changeset/30119-studio-customization-environment-line.md new file mode 100644 index 00000000000..7a340f418a9 --- /dev/null +++ b/.changeset/30119-studio-customization-environment-line.md @@ -0,0 +1,11 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +--- + +feat(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 207bb9f32f9..f764b8daec4 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'; @@ -1401,7 +1401,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; } @@ -1697,6 +1699,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 f7f66efbae5..42fe26f6fe0 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -4941,6 +4941,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 decad288cde..86f48e149f5 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -4536,6 +4536,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 9ba9c6ae612..f6283c84ee1 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -3993,6 +3993,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 f22af5ce0c2..a87278f69a6 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -4638,6 +4638,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 c216cdab8b9..cfd375667af 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -4634,6 +4634,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 2c2f9ba76a8..48d34a7ac3a 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -4538,6 +4538,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 530c8bb2d76..c595db4eea6 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -4535,6 +4535,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 c9c734753f3..90d5ba64c85 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -4633,6 +4633,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 f251e5e8654..e23daea4c03 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -4749,6 +4749,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 c7f54ab82ad..a877268ede6 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -3457,6 +3457,7 @@ const zh = { title: '待发布的变更', description: '发布将带来以下变更。“新增”为全新内容,“更新”将覆盖线上版本。', savedToCustomization: '保存到 {{package}} 的定制', + customizationEnvironment: '这些变更保存在当前环境的数据库里,是 {{pair}} 的条目,不在项目源码中;只有通过 {{command}} 才能带到其他环境。', loading: '正在加载待发布变更…', loadFailed: '无法加载待发布变更:', empty: '没有待发布的内容 —— 所有草稿均已发布。', From 8fc8d012d3e07f23593d08ee92ee9a2b8c48cd51 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 10:46:37 +0000 Subject: [PATCH 2/2] fix(app-shell): name the changeset as the card's kind Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Utp6Cay8ZjjPDJ1vkTcVDd --- .changeset/30119-studio-customization-environment-line.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/30119-studio-customization-environment-line.md b/.changeset/30119-studio-customization-environment-line.md index 7a340f418a9..02b055a3ba6 100644 --- a/.changeset/30119-studio-customization-environment-line.md +++ b/.changeset/30119-studio-customization-environment-line.md @@ -3,7 +3,7 @@ '@object-ui/i18n': patch --- -feat(app-shell): Studio's publish review sheet says a customization lives in this environment's database and moves only by `os package export` (#30119) +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