From b4fe63493a9f18ef3874a85fba392ff24a8f3aa8 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 5 Nov 2026 13:36:05 +0000
Subject: [PATCH] fix(app-shell): Studio item header's saved-to note says where
the customization lives (#30436)
WIP before install/build.
Claude-Session: https://claude.ai/code/session_01L6bhgTL7xvsd9AqyDZgX3C
Co-authored-by: Claude
---
.../30436-studio-saved-to-environment.md | 10 +++++++
.../src/preview/savedToCustomization.tsx | 29 +++++++++++++++++--
...mationsPillar.customizeFlow-28570.test.tsx | 6 ++++
...aPillar.baseObjectExtension-28575.test.tsx | 7 +++++
.../InterfacesPillar.customize-28562.test.tsx | 4 +++
...ignSurface.customizePublish-28562.test.tsx | 8 +++++
.../studio-design/StudioDesignSurface.tsx | 12 ++++++--
7 files changed, 70 insertions(+), 6 deletions(-)
create mode 100644 .changeset/30436-studio-saved-to-environment.md
diff --git a/.changeset/30436-studio-saved-to-environment.md b/.changeset/30436-studio-saved-to-environment.md
new file mode 100644
index 00000000000..3fcd88e987f
--- /dev/null
+++ b/.changeset/30436-studio-saved-to-environment.md
@@ -0,0 +1,10 @@
+---
+'@object-ui/app-shell': patch
+---
+
+fix(app-shell): the Studio item header's "Saved to X's customizations" note says where the customization lives, in the publish sheet's own sentence (#30436)
+
+Clause-②: no
+Metadata-protocol: none
+
+The note on an item header (Interfaces item, navigation editor, Data object, Automations flow) now goes on with the sentence the pending-changes sheet draws under it (`preview.changes.customizationEnvironment`, from #30119): the changes are kept in this environment's database as items of the real `.custom` pair, and reach another environment only through `os package export`. No new i18n key: the note reuses `customizationEnvironmentText`. Before the pair's first save the note names the `.custom` id that save creates.
diff --git a/ui/packages/app-shell/src/preview/savedToCustomization.tsx b/ui/packages/app-shell/src/preview/savedToCustomization.tsx
index 74243347c7e..db0229bc6cf 100644
--- a/ui/packages/app-shell/src/preview/savedToCustomization.tsx
+++ b/ui/packages/app-shell/src/preview/savedToCustomization.tsx
@@ -46,8 +46,31 @@ export function customizationEnvironmentText(t: Translate, pairId: string): stri
});
}
-/** {@link savedToCustomizationText} as an element, for the Studio item header's note line. */
-export function SavedToCustomizationNote({ baseLabel, testId }: { baseLabel: string; testId?: string }) {
+/**
+ * {@link savedToCustomizationText} as an element, for the Studio item header's
+ * note line. Given the pair's id, it goes on with the pending-changes sheet's
+ * second sentence, {@link customizationEnvironmentText}, so the header and
+ * the sheet say the same words from the same keys (objectstack#30436).
+ */
+export function SavedToCustomizationNote({
+ baseLabel,
+ pairId,
+ testId,
+}: {
+ baseLabel: string;
+ /** The base's `.custom` pair; nothing more is said without one. */
+ pairId?: string | null;
+ testId?: string;
+}) {
const { t } = useObjectTranslation();
- return {savedToCustomizationText(t, baseLabel)};
+ return (
+ <>
+ {savedToCustomizationText(t, baseLabel)}
+ {pairId ? (
+
+ {customizationEnvironmentText(t, pairId)}
+
+ ) : null}
+ >
+ );
}
diff --git a/ui/packages/app-shell/src/views/studio-design/AutomationsPillar.customizeFlow-28570.test.tsx b/ui/packages/app-shell/src/views/studio-design/AutomationsPillar.customizeFlow-28570.test.tsx
index 56355245561..aaabd1bc939 100644
--- a/ui/packages/app-shell/src/views/studio-design/AutomationsPillar.customizeFlow-28570.test.tsx
+++ b/ui/packages/app-shell/src/views/studio-design/AutomationsPillar.customizeFlow-28570.test.tsx
@@ -504,6 +504,10 @@ describe('Customize a read-only base’s flow into its pair (objectstack#28570)'
describe('the flow header says where a customized flow is saved (objectstack#29691)', () => {
const SAVED_TO = 'Saved to HotCRM’s customizations';
const savedTo = () => within(header()).queryByTestId('auto-flow-saved-to');
+ // objectstack#30436 — and the sheet's second sentence, naming the real pair.
+ const ENVIRONMENT =
+ 'These changes are kept in this environment’s database as items of com.example.hotcrm.custom, not in your project’s source. They reach another environment only through os package export.';
+ const environment = () => within(header()).queryByTestId('auto-flow-saved-to-environment');
it('Customize unlocks the flow, and the header says it is saved to the base’s customizations', async () => {
mount();
@@ -511,6 +515,7 @@ describe('the flow header says where a customized flow is saved (objectstack#296
expect(savedTo()).toBeNull();
await customize();
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
+ expect(environment()).toHaveTextContent(ENVIRONMENT);
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
});
@@ -520,6 +525,7 @@ describe('the flow header says where a customized flow is saved (objectstack#296
mount({ listedPair: PAIR });
await openFlow(/^Notify owner and manager$/);
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
+ expect(environment()).toHaveTextContent(ENVIRONMENT);
});
it('CONTROL: the base’s flow before Customize says nothing of the kind', async () => {
diff --git a/ui/packages/app-shell/src/views/studio-design/DataPillar.baseObjectExtension-28575.test.tsx b/ui/packages/app-shell/src/views/studio-design/DataPillar.baseObjectExtension-28575.test.tsx
index af4874102db..9a5e3008f92 100644
--- a/ui/packages/app-shell/src/views/studio-design/DataPillar.baseObjectExtension-28575.test.tsx
+++ b/ui/packages/app-shell/src/views/studio-design/DataPillar.baseObjectExtension-28575.test.tsx
@@ -450,6 +450,11 @@ describe('A Chinese label names a base object\'s extension field by pinyin, suff
describe('the object header says where a base object’s extension is saved (objectstack#29691)', () => {
const SAVED_TO = 'Saved to HotCRM’s customizations';
const savedTo = () => within(screen.getByTestId('data-object-header')).queryByTestId('data-object-saved-to');
+ // objectstack#30436 — and the sheet's second sentence, naming the real pair.
+ const ENVIRONMENT =
+ 'These changes are kept in this environment’s database as items of com.example.hotcrm.custom, not in your project’s source. They reach another environment only through os package export.';
+ const environment = () =>
+ within(screen.getByTestId('data-object-header')).queryByTestId('data-object-saved-to-environment');
it('the first add makes the extension, and the header says it is saved to the base’s customizations', async () => {
await renderBase();
@@ -458,6 +463,7 @@ describe('the object header says where a base object’s extension is saved (obj
await addFromFormToolbar('Region');
await waitFor(() => expect(extensionSaves()).toHaveLength(1), { timeout: 8000 });
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
+ expect(environment()).toHaveTextContent(ENVIRONMENT);
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
});
@@ -467,6 +473,7 @@ describe('the object header says where a base object’s extension is saved (obj
srv.publishedExt = { extend: OBJECT, fields: { region__c: region }, _packageId: PAIR };
await renderBase();
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
+ expect(environment()).toHaveTextContent(ENVIRONMENT);
});
it('CONTROL: a base object with no extension says nothing of the kind', async () => {
diff --git a/ui/packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx b/ui/packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx
index 838d1d01063..f6fa555018d 100644
--- a/ui/packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx
+++ b/ui/packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx
@@ -782,6 +782,10 @@ describe('Customize a read-only base’s navigation into its pair (objectstack#3
await openPipeline();
fireEvent.click(await within(rail()).findByTestId('if-edit-nav', undefined, { timeout: 8000 }));
expect(await within(rail()).findByTestId('if-nav-saved-to')).toBeInTheDocument();
+ // objectstack#30436 — and the sheet's second sentence, naming the pair.
+ expect(within(rail()).getByTestId('if-nav-saved-to-environment')).toHaveTextContent(
+ 'These changes are kept in this environment’s database as items of com.example.hotcrm.custom, not in your project’s source. They reach another environment only through os package export.',
+ );
await sleep(1500);
expect(srv.ensure).toEqual([]);
expect(srv.saves).toEqual([]);
diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.customizePublish-28562.test.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.customizePublish-28562.test.tsx
index c485425ef30..62ca2d81c92 100644
--- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.customizePublish-28562.test.tsx
+++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.customizePublish-28562.test.tsx
@@ -355,12 +355,18 @@ describe('the wall is read from what the server publishes (objectstack#28562)',
describe('the item header says where the customization is saved (objectstack#29691)', () => {
const SAVED_TO = 'Saved to HotCRM’s customizations';
const savedTo = () => within(header()).queryByTestId('if-item-saved-to');
+ // objectstack#30436 — and the sheet's second sentence, naming the real pair.
+ const ENVIRONMENT =
+ 'These changes are kept in this environment’s database as items of com.example.hotcrm.custom, not in your project’s source. They reach another environment only through os package export.';
+ const environment = () => within(header()).queryByTestId('if-item-saved-to-environment');
it('after Customize the header says it, and after the first edit it still does, in the sheet’s own words', async () => {
renderWorkbench();
await openPipeline();
fireEvent.click(await within(header()).findByTestId('studio-customize', undefined, { timeout: 8000 }));
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
+ // Before the first save the pair is the `.custom` id that save creates.
+ expect(environment()).toHaveTextContent(ENVIRONMENT);
// On its own line under the title row, never in it (the fold measures the row).
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
expect(within(header()).getByRole('heading', { level: 1 }).parentElement).not.toContainElement(savedTo());
@@ -369,6 +375,7 @@ describe('the item header says where the customization is saved (objectstack#296
await waitFor(() => expect(srv.saves).toHaveLength(1), { timeout: 8000 });
await waitFor(() => expect(panelPackage()).toBe(PAIR), { timeout: 8000 });
expect(savedTo()).toHaveTextContent(SAVED_TO);
+ expect(environment()).toHaveTextContent(ENVIRONMENT);
// The header line and the sheet line read the same.
expect(screen.getByTestId('sheet-saved-to').textContent).toBe(savedTo()!.textContent);
});
@@ -378,6 +385,7 @@ describe('the item header says where the customization is saved (objectstack#296
await openPipeline();
await within(header()).findByTestId('studio-customize', undefined, { timeout: 8000 });
expect(savedTo()).toBeNull();
+ expect(environment()).toBeNull();
expect(header().querySelector('[data-slot="item-note"]')).toBeNull();
expect(screen.queryByTestId('sheet-saved-to')).toBeNull();
});
diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx
index 9e7305aa34c..daff7ee2211 100644
--- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx
+++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx
@@ -2862,6 +2862,9 @@ export function InterfacesPillar({
const customizeBase = customizing && customization ? customization.base : null;
const customizePair = customizing && customization ? customization.pairId : null;
const customizeBaseLabel = customization?.baseLabel ?? packageId;
+ // objectstack#30436 — the pair the "saved to" note names: the served one, or
+ // the `.custom` id the first save creates.
+ const customizeNotePair = customization ? (customization.pairId ?? pairIdOf(customization.base)) : null;
const customizeWalled = customizing && customization?.walled === true;
const customizeBlock: CustomizeBlock = customizeWalled
? 'walled'
@@ -4235,7 +4238,7 @@ export function InterfacesPillar({
// objectstack#29693 — on a narrow row the chips and the item tools fold
// into the ⋯ menu before the title drops below its readable width.
foldMenu={itemMoreEl ? 'pillar' : { label: t('engine.studio.if.more', locale), testId: 'if-item-header-more' }}
- note={ifSavedTo ? : undefined}
+ note={ifSavedTo ? : undefined}
leading={