Skip to content

Commit c7840ea

Browse files
fix(app-shell): Studio item header's saved-to note says where the customization lives (#30436) (#30523)
domain:ui#5 · os-sales · session_01L6bhgTL7xvsd9AqyDZgX3C <!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuCtrgd3H9uqx7XY1em1jhdt)_ Fixes #30436 ## What changes - `SavedToCustomizationNote` (`ui/packages/app-shell/src/preview/savedToCustomization.tsx`) takes a new optional `pairId` prop. Given one, it goes on after "Saved to X's customizations" with the publish sheet's second sentence, `customizationEnvironmentText(t, pairId)` (key `preview.changes.customizationEnvironment`, from #30435): the changes are kept in this environment's database as items of the pair and reach another environment only through `os package export`. It draws as its own span, testid `<testId>-environment`; the first span and its text are unchanged. - `StudioDesignSurface.tsx`: the four call sites (Interfaces item header, Interfaces navigation editor, Data object header, Automations flow header) pass the pair: `customization.pairId`, or `pairIdOf(customization.base)` (the `<base>.custom` id the first save creates) while the base has no pair yet. Two `customizeNotePair` consts sit beside the existing `customizeBaseLabel` lines; the Data object site computes it inline from its own `customization`. - `DraftChangesPanel.tsx` is not touched. No new i18n key. ## Why The item header said only where a customization is saved, while the publish sheet also says where it lives and how it moves (ADR-0146 D2). The two now say the same words from the same keys. ## Verification - `pnpm exec vitest run` (from `ui/`) on the four pillar test files and `DraftChangesPanel.test.tsx`: 5 files, 91 tests pass. New assertions check the full English sentence naming `com.example.hotcrm.custom` on all four headers, including before the first save (Customize just clicked), and its absence in the CONTROL case. - Ablation: with the sentence disabled in the component, 6 of the new assertions fail across the 4 pillar files. - `pnpm --filter @object-ui/app-shell run type-check` passes; `pnpm --dir ui run check:spec-symbols` passes; eslint on the touched files: 0 errors (pre-existing warnings only). - `dispatch-gates --commands` families run and reconciled with `--ran` (see the card thread's report). Clause-②: no Metadata-protocol: none ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgTL7xvsd9AqyDZgX3C Co-authored-by: Claude <noreply@anthropic.com>
1 parent 43faa22 commit c7840ea

7 files changed

Lines changed: 70 additions & 6 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
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)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
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 `<base>.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 `<base>.custom` id that save creates.

‎ui/packages/app-shell/src/preview/savedToCustomization.tsx‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,31 @@ export function customizationEnvironmentText(t: Translate, pairId: string): stri
4646
});
4747
}
4848

49-
/** {@link savedToCustomizationText} as an element, for the Studio item header's note line. */
50-
export function SavedToCustomizationNote({ baseLabel, testId }: { baseLabel: string; testId?: string }) {
49+
/**
50+
* {@link savedToCustomizationText} as an element, for the Studio item header's
51+
* note line. Given the pair's id, it goes on with the pending-changes sheet's
52+
* second sentence, {@link customizationEnvironmentText}, so the header and
53+
* the sheet say the same words from the same keys (objectstack#30436).
54+
*/
55+
export function SavedToCustomizationNote({
56+
baseLabel,
57+
pairId,
58+
testId,
59+
}: {
60+
baseLabel: string;
61+
/** The base's `<base>.custom` pair; nothing more is said without one. */
62+
pairId?: string | null;
63+
testId?: string;
64+
}) {
5165
const { t } = useObjectTranslation();
52-
return <span data-testid={testId}>{savedToCustomizationText(t, baseLabel)}</span>;
66+
return (
67+
<>
68+
<span data-testid={testId}>{savedToCustomizationText(t, baseLabel)}</span>
69+
{pairId ? (
70+
<span data-testid={testId ? `${testId}-environment` : undefined} className="block">
71+
{customizationEnvironmentText(t, pairId)}
72+
</span>
73+
) : null}
74+
</>
75+
);
5376
}

‎ui/packages/app-shell/src/views/studio-design/AutomationsPillar.customizeFlow-28570.test.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,13 +504,18 @@ describe('Customize a read-only base’s flow into its pair (objectstack#28570)'
504504
describe('the flow header says where a customized flow is saved (objectstack#29691)', () => {
505505
const SAVED_TO = 'Saved to HotCRM’s customizations';
506506
const savedTo = () => within(header()).queryByTestId('auto-flow-saved-to');
507+
// objectstack#30436 — and the sheet's second sentence, naming the real pair.
508+
const ENVIRONMENT =
509+
'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.';
510+
const environment = () => within(header()).queryByTestId('auto-flow-saved-to-environment');
507511

508512
it('Customize unlocks the flow, and the header says it is saved to the base’s customizations', async () => {
509513
mount();
510514
await openFlow();
511515
expect(savedTo()).toBeNull();
512516
await customize();
513517
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
518+
expect(environment()).toHaveTextContent(ENVIRONMENT);
514519
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
515520
});
516521

@@ -520,6 +525,7 @@ describe('the flow header says where a customized flow is saved (objectstack#296
520525
mount({ listedPair: PAIR });
521526
await openFlow(/^Notify owner and manager$/);
522527
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
528+
expect(environment()).toHaveTextContent(ENVIRONMENT);
523529
});
524530

525531
it('CONTROL: the base’s flow before Customize says nothing of the kind', async () => {

‎ui/packages/app-shell/src/views/studio-design/DataPillar.baseObjectExtension-28575.test.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,11 @@ describe('A Chinese label names a base object\'s extension field by pinyin, suff
450450
describe('the object header says where a base object’s extension is saved (objectstack#29691)', () => {
451451
const SAVED_TO = 'Saved to HotCRM’s customizations';
452452
const savedTo = () => within(screen.getByTestId('data-object-header')).queryByTestId('data-object-saved-to');
453+
// objectstack#30436 — and the sheet's second sentence, naming the real pair.
454+
const ENVIRONMENT =
455+
'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.';
456+
const environment = () =>
457+
within(screen.getByTestId('data-object-header')).queryByTestId('data-object-saved-to-environment');
453458

454459
it('the first add makes the extension, and the header says it is saved to the base’s customizations', async () => {
455460
await renderBase();
@@ -458,6 +463,7 @@ describe('the object header says where a base object’s extension is saved (obj
458463
await addFromFormToolbar('Region');
459464
await waitFor(() => expect(extensionSaves()).toHaveLength(1), { timeout: 8000 });
460465
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
466+
expect(environment()).toHaveTextContent(ENVIRONMENT);
461467
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
462468
});
463469

@@ -467,6 +473,7 @@ describe('the object header says where a base object’s extension is saved (obj
467473
srv.publishedExt = { extend: OBJECT, fields: { region__c: region }, _packageId: PAIR };
468474
await renderBase();
469475
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
476+
expect(environment()).toHaveTextContent(ENVIRONMENT);
470477
});
471478

472479
it('CONTROL: a base object with no extension says nothing of the kind', async () => {

‎ui/packages/app-shell/src/views/studio-design/InterfacesPillar.customize-28562.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,10 @@ describe('Customize a read-only base’s navigation into its pair (objectstack#3
782782
await openPipeline();
783783
fireEvent.click(await within(rail()).findByTestId('if-edit-nav', undefined, { timeout: 8000 }));
784784
expect(await within(rail()).findByTestId('if-nav-saved-to')).toBeInTheDocument();
785+
// objectstack#30436 — and the sheet's second sentence, naming the pair.
786+
expect(within(rail()).getByTestId('if-nav-saved-to-environment')).toHaveTextContent(
787+
'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.',
788+
);
785789
await sleep(1500);
786790
expect(srv.ensure).toEqual([]);
787791
expect(srv.saves).toEqual([]);

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.customizePublish-28562.test.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,18 @@ describe('the wall is read from what the server publishes (objectstack#28562)',
355355
describe('the item header says where the customization is saved (objectstack#29691)', () => {
356356
const SAVED_TO = 'Saved to HotCRM’s customizations';
357357
const savedTo = () => within(header()).queryByTestId('if-item-saved-to');
358+
// objectstack#30436 — and the sheet's second sentence, naming the real pair.
359+
const ENVIRONMENT =
360+
'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.';
361+
const environment = () => within(header()).queryByTestId('if-item-saved-to-environment');
358362

359363
it('after Customize the header says it, and after the first edit it still does, in the sheet’s own words', async () => {
360364
renderWorkbench();
361365
await openPipeline();
362366
fireEvent.click(await within(header()).findByTestId('studio-customize', undefined, { timeout: 8000 }));
363367
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
368+
// Before the first save the pair is the `<base>.custom` id that save creates.
369+
expect(environment()).toHaveTextContent(ENVIRONMENT);
364370
// On its own line under the title row, never in it (the fold measures the row).
365371
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
366372
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
369375
await waitFor(() => expect(srv.saves).toHaveLength(1), { timeout: 8000 });
370376
await waitFor(() => expect(panelPackage()).toBe(PAIR), { timeout: 8000 });
371377
expect(savedTo()).toHaveTextContent(SAVED_TO);
378+
expect(environment()).toHaveTextContent(ENVIRONMENT);
372379
// The header line and the sheet line read the same.
373380
expect(screen.getByTestId('sheet-saved-to').textContent).toBe(savedTo()!.textContent);
374381
});
@@ -378,6 +385,7 @@ describe('the item header says where the customization is saved (objectstack#296
378385
await openPipeline();
379386
await within(header()).findByTestId('studio-customize', undefined, { timeout: 8000 });
380387
expect(savedTo()).toBeNull();
388+
expect(environment()).toBeNull();
381389
expect(header().querySelector('[data-slot="item-note"]')).toBeNull();
382390
expect(screen.queryByTestId('sheet-saved-to')).toBeNull();
383391
});

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2862,6 +2862,9 @@ export function InterfacesPillar({
28622862
const customizeBase = customizing && customization ? customization.base : null;
28632863
const customizePair = customizing && customization ? customization.pairId : null;
28642864
const customizeBaseLabel = customization?.baseLabel ?? packageId;
2865+
// objectstack#30436 — the pair the "saved to" note names: the served one, or
2866+
// the `<base>.custom` id the first save creates.
2867+
const customizeNotePair = customization ? (customization.pairId ?? pairIdOf(customization.base)) : null;
28652868
const customizeWalled = customizing && customization?.walled === true;
28662869
const customizeBlock: CustomizeBlock = customizeWalled
28672870
? 'walled'
@@ -4235,7 +4238,7 @@ export function InterfacesPillar({
42354238
// objectstack#29693 — on a narrow row the chips and the item tools fold
42364239
// into the ⋯ menu before the title drops below its readable width.
42374240
foldMenu={itemMoreEl ? 'pillar' : { label: t('engine.studio.if.more', locale), testId: 'if-item-header-more' }}
4238-
note={ifSavedTo ? <SavedToCustomizationNote baseLabel={customizeBaseLabel} testId="if-item-saved-to" /> : undefined}
4241+
note={ifSavedTo ? <SavedToCustomizationNote baseLabel={customizeBaseLabel} pairId={customizeNotePair} testId="if-item-saved-to" /> : undefined}
42394242
leading={
42404243
<button
42414244
type="button"
@@ -5139,7 +5142,7 @@ export function InterfacesPillar({
51395142
{/* objectstack#30059 — where a read-only base's navigation edit is saved. */}
51405143
{editNav && readOnly && (
51415144
<p className="mt-1 text-[10px] text-muted-foreground">
5142-
<SavedToCustomizationNote baseLabel={customizeBaseLabel} testId="if-nav-saved-to" />
5145+
<SavedToCustomizationNote baseLabel={customizeBaseLabel} pairId={customizeNotePair} testId="if-nav-saved-to" />
51435146
</p>
51445147
)}
51455148
{listOnlyCustomized && !editNav && !navMarked ? null : appStatus === 'missing' && !error ? (
@@ -7003,6 +7006,7 @@ export function DataPillar({
70037006
(extOpen !== null && (extOpen.published.size > 0 || extHasDraft || extDirty))) ? (
70047007
<SavedToCustomizationNote
70057008
baseLabel={customization.baseLabel}
7009+
pairId={customization.pairId ?? pairIdOf(customization.base)}
70067010
testId="data-object-saved-to"
70077011
/>
70087012
) : undefined
@@ -7807,6 +7811,8 @@ export function AutomationsPillar({
78077811
const customizeBase = flowCustomizing && customization ? customization.base : null;
78087812
const customizePair = flowCustomizing && customization ? customization.pairId : null;
78097813
const customizeBaseLabel = customization?.baseLabel ?? railPackageId ?? '';
7814+
// objectstack#30436 — the pair the "saved to" note names.
7815+
const customizeNotePair = customization ? (customization.pairId ?? pairIdOf(customization.base)) : null;
78107816
const customizeWalled = flowCustomizing && customization?.walled === true;
78117817
const customizeBlock: CustomizeBlock = customizeWalled
78127818
? 'walled'
@@ -9053,7 +9059,7 @@ export function AutomationsPillar({
90539059
// or is unlocked to customize saves to the base's pair.
90549060
note={
90559061
customization !== null && (!!current?.addedIn || flowInPair || flowUnlocked) ? (
9056-
<SavedToCustomizationNote baseLabel={customizeBaseLabel} testId="auto-flow-saved-to" />
9062+
<SavedToCustomizationNote baseLabel={customizeBaseLabel} pairId={customizeNotePair} testId="auto-flow-saved-to" />
90579063
) : undefined
90589064
}
90599065
leading={

0 commit comments

Comments
 (0)