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={