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