Skip to content

Commit 97f8f6f

Browse files
feat(app-shell): the Studio item header says where a read-only base’s customization is saved (#29843)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbUbQXNJDPax1UE5upP9BnZj)_ Fixes #29691 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:Studio 条目标题栏在标题行下方、标签页上方多一行小字「保存到 基座名 的定制」,与待发布变更面板(PR 1,#29723)同一句话、同一个文案键。出现在 Interfaces(定制包新增、已覆盖或已点「定制」解锁的条目)、Data(定制包新增的对象,或已有定制扩展的基座对象)、Automations(定制包新增、已覆盖或已解锁的流程)。Access 不变,它的标签已写明基座。 - 为什么改:定制只读软件包时,标题栏只说「定制」,不说改动存到哪里;面板说了,标题栏没说(ADR-0146 D3)。 - 风险与回滚:仅 UI,无新文案键;这一行在标题行之外,#29693 的折叠只量标题行,不受影响(有消融测试)。回滚即还原本 PR,无数据或协议变化。 - 先看:`StudioItemHeader.tsx` 的 `note` 槽位(标题行之后);新文件 `preview/savedToCustomization.tsx`(这句话与 `SavedToCustomizationNote`,`DraftChangesPanel` 改为转出);`StudioDesignSurface.tsx` 三处 `note=` 的显示条件。 - 两分钟验证:在 showcase 打开任一视图,点「定制」,标题下出现「Saved to ObjectStack Showcase’s customizations」;改一处再点 Publish,面板标题下是同一句话。可写软件包的条目没有这一行。 - 需要你做的:无,席位落地。 ## What changes - **Item header note (ADR-0146 D3).** `StudioItemHeader` takes an optional `note`. It is one line under the title row and above the tabs, outside the title row, so the objectstack#29693 fold (which measures only that row) never counts it. - **One sentence, in its own module.** `savedToCustomizationText` (from PR 1, #29723) moves to `preview/savedToCustomization.tsx`, next to a new `SavedToCustomizationNote` element. `DraftChangesPanel` imports it and re-exports it, so its public export is unchanged. The header and the pending-changes sheet say the same words from the same key (`preview.changes.savedToCustomization`), and no key is added. The move is needed because many Studio tests replace the whole `DraftChangesPanel` module with a stub. When the header imported the note from that module, 7 tests in `oneSurface-28572` and `kernelReadOnly-29567` crashed with "No SavedToCustomizationNote export is defined on the mock". The header now does not depend on what such a stub exports. - **Where it shows** (`StudioDesignSurface.tsx`), only on a read-only base with a customization: - **Interfaces:** an item the pair adds, an item the pair overrides, or one unlocked by Customize. - **Data:** an object the pair adds, or a base object the pair's extension already extends (published, drafted or edited). - **Automations:** a flow the pair adds, a flow the pair overrides, or one unlocked by Customize. - The label is the base's (`customization.baseLabel`). On Data, `packageId` names the pair for a pair's own object, so it is not used. - **Not changed:** Access, whose chip already names the base; the publish toast; `DraftChangesPanel.test.tsx`. ## Contract review - **What widens:** `@object-ui/app-shell` gains an optional `StudioItemHeader` prop (`note`) and a source module `preview/savedToCustomization.tsx` exporting `SavedToCustomizationNote` and `savedToCustomizationText`. `DraftChangesPanel` still exports `savedToCustomizationText`. - **What narrows:** nothing. No key, prop, export or accepted value is removed or tightened. Metadata authors see no change (`Metadata-protocol: none`). ## Tests - `StudioItemHeader.titleRowFold-12897.test.tsx`: a new describe over the stubbed-geometry harness. The note is the title row's next sibling and is not inside it, with the tab row after it. A 400 px note, wider than the room the row leaves, folds nothing at 632 px. The row still folds at 330 px and unfolds at 480 px with the note there. CONTROL: no note, no line. - `StudioDesignSurface.customizePublish-28562.test.tsx` (the real surface): after Customize, the Interfaces header says "Saved to HotCRM’s customizations" outside the title row, and still does after the first edit makes the pair. The header line and the sheet line read the same. CONTROL: before Customize, no line. - `DataPillar.baseObjectExtension-28575.test.tsx` (the real pillar): the first Add field makes the extension and the object header says it; a base object the published extension extends says it on open. CONTROLS: a base object with no extension, and a writable package's object, say nothing. - `AutomationsPillar.customizeFlow-28570.test.tsx` (the real pillar): Customize unlocks the flow and the header says it; a published override says it on open. CONTROLS: the base's flow before Customize, and a writable package's flow, say nothing. - On the same-text check: this harness stubs the sheet (`DraftChangesPanel`). The stub now draws its line with the real `savedToCustomizationText`, using the label the host hands it. The pin compares the real header element with the sentence the real sheet draws from that label. `DraftChangesPanel.test.tsx` from PR 1 pins the real sheet's own line. ## Verification <details><summary>Tests and gates (code head a3d4ffd; head e2d07e0 only rewords the changeset, and its changeset gates exit 0)</summary> - **Red on main, green here.** I ran the four touched test files against `origin/main` `339f63da`'s `StudioItemHeader.tsx`, `DraftChangesPanel.tsx` and `StudioDesignSurface.tsx`, keeping the new helper module the test imports, and restored `HEAD` after. Result: 6 failed, 52 passed. The 6 are the new surface and header pins, and every CONTROL stays green on main. On the branch: 58 passed. - **Ablation.** Moving the note inside the title row (the row's last child) reds 3 of the 4 note cases in the fold describe: the own-line case, "a note wider than the free room folds nothing" and the follow-the-row case. CONTROL stays green. Restored after. - **Sweep:** every ui test file naming `StudioItemHeader`, `DraftChangesPanel`, `StudioDesignSurface`, `DataPillar`, `AutomationsPillar`, `InterfacesPillar` or `savedToCustomization` (284 files, in six locked batches, five of them with `src/preview/` whole alongside), all green on `a3d4ffd1`: 358 + 701 + 528 + 509 + 655 + 800 tests. An earlier sweep on `6e6a5f34` was what caught the 7 crashes the module move fixes. - **Type check:** `turbo run type-check --filter=@object-ui/app-shell`: 65 of 65 tasks green. - **eslint** on the touched source and test files: 0 errors. The one new warning is react-refresh's only-export-components on `savedToCustomizationText` in the new module, the same warning that export carried in `DraftChangesPanel`. - **Derived gates** (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`): 35 commands, each exit 0 on `a3d4ffd1`, including `check-sdui-manifest`, `check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity` (run against this body) and `pnpm --dir ui exec vitest run scripts/`. - **Not run locally:** the full Test UI shards, which CI's Test UI runs, and a live browser pass. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTWKHRdA5msPPhwmw4)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bbb98c4 commit 97f8f6f

9 files changed

Lines changed: 288 additions & 14 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
feat(app-shell): the Studio item header says where a customization of a read-only base is saved
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
10+
ADR-0146 D3 saves every edit of a read-only base into its `.custom` pair. The Studio item header now says so, in the pending-changes sheet's own words: "Saved to BASE's customizations" (zh: 保存到 BASE 的定制). It shows on one line under the title row, above the tabs. It appears on Interfaces for an item the pair adds, overrides or has been unlocked by Customize; on Data for an object the pair adds or a base object the pair's extension already extends; and on Automations for a flow the pair adds, overrides or has been unlocked. Access is unchanged, since its chip already names the base.
11+
12+
`StudioItemHeader` takes an optional `note` prop, drawn outside the title row so the objectstack#29693 fold never measures it. The sentence moves to its own module, `preview/savedToCustomization.tsx`, beside a new `SavedToCustomizationNote` element, so the header does not depend on the pending-changes sheet's module; `DraftChangesPanel` still exports `savedToCustomizationText`. No i18n key is added, and nothing is removed or narrowed.

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import { readDraftChanges, type DraftChange, type MetadataClient } from '@object
8282
// spec-named local declaration is what `scripts/check-spec-symbol-derivation.mjs`
8383
// refuses, and a faithful copy is exactly the fork that guard exists to prevent.
8484
import { fetchPendingDrafts } from './usePendingDrafts.js';
85+
import { savedToCustomizationText } from './savedToCustomization.js';
8586
import { canonicalMetaUrlType } from '@objectstack/spec/shared';
8687
import { resolveI18nLabel, type I18nLabel } from '@objectstack/spec/ui';
8788
import { MetadataProtectionFields, OverrideDeclarationFields, stripReadDecorations } from '@objectstack/spec/kernel';
@@ -1384,17 +1385,11 @@ export interface DraftChangesPanelProps {
13841385
export type DraftChangesPosture = 'review' | 'confirm';
13851386

13861387
/**
1387-
* objectstack ADR-0146 D3 — the one sentence that names where a customization
1388-
* of a read-only base is saved: "Saved to BASE's customizations". The
1389-
* pending-changes sheet draws it, and the item header reuses it, so both say
1390-
* the same words.
1388+
* objectstack ADR-0146 D3 — the "Saved to BASE's customizations" sentence,
1389+
* from its own module so the item header can say it without this sheet's
1390+
* module (objectstack#29691). Re-exported here, where #29723 published it.
13911391
*/
1392-
export function savedToCustomizationText(t: Translate, baseLabel: string): string {
1393-
return t('preview.changes.savedToCustomization', {
1394-
package: baseLabel,
1395-
defaultValue: 'Saved to {{package}}’s customizations',
1396-
});
1397-
}
1392+
export { savedToCustomizationText };
13981393

13991394
export function DraftChangesPanel({
14001395
open,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack ADR-0146 D3 — the one sentence that names where a customization
5+
* of a read-only base is saved: "Saved to BASE's customizations". The
6+
* pending-changes sheet (`DraftChangesPanel`) draws it under its title, and
7+
* the Studio item header draws it on its note line (objectstack#29691), so
8+
* both say the same words from the same key.
9+
*
10+
* Its own module, not the sheet's: many Studio tests replace the whole
11+
* `DraftChangesPanel` module with a stub, and the header must not depend on
12+
* what such a stub happens to export.
13+
*/
14+
15+
import { useObjectTranslation } from '@object-ui/i18n';
16+
17+
type Translate = ReturnType<typeof useObjectTranslation>['t'];
18+
19+
export function savedToCustomizationText(t: Translate, baseLabel: string): string {
20+
return t('preview.changes.savedToCustomization', {
21+
package: baseLabel,
22+
defaultValue: 'Saved to {{package}}’s customizations',
23+
});
24+
}
25+
26+
/** {@link savedToCustomizationText} as an element, for the Studio item header's note line. */
27+
export function SavedToCustomizationNote({ baseLabel, testId }: { baseLabel: string; testId?: string }) {
28+
const { t } = useObjectTranslation();
29+
return <span data-testid={testId}>{savedToCustomizationText(t, baseLabel)}</span>;
30+
}

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

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
* - a walled tenant admin sees Customize disabled, and nothing is asked;
2727
* - a refused save and a refused publish read as the server sent them;
2828
* - CONTROLS: the base's flow with no override offers no Restore; a
29-
* writable package's flow keeps Delete flow and its definition switch.
29+
* writable package's flow keeps Delete flow and its definition switch;
30+
* - objectstack#29691: a customized flow (unlocked, or a published
31+
* override) has the header say where it is saved ("Saved to HotCRM’s
32+
* customizations"); the base's flow before Customize, and a writable
33+
* package's flow, say nothing of the kind.
3034
*/
3135

3236
import '@testing-library/jest-dom/vitest';
@@ -467,3 +471,39 @@ describe('Customize a read-only base’s flow into its pair (objectstack#28570)'
467471
expect(screen.queryByTestId('studio-restore-original')).toBeNull();
468472
});
469473
});
474+
475+
describe('the flow header says where a customized flow is saved (objectstack#29691)', () => {
476+
const SAVED_TO = 'Saved to HotCRM’s customizations';
477+
const savedTo = () => within(header()).queryByTestId('auto-flow-saved-to');
478+
479+
it('Customize unlocks the flow, and the header says it is saved to the base’s customizations', async () => {
480+
mount();
481+
await openFlow();
482+
expect(savedTo()).toBeNull();
483+
await customize();
484+
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
485+
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
486+
});
487+
488+
it('a published override says it on open', async () => {
489+
srv.pairActive = { ...strip(BASE_FLOW), label: 'Notify owner and manager', overrides: DECLARATION };
490+
srv.pairExists = true;
491+
mount({ listedPair: PAIR });
492+
await openFlow(/^Notify owner and manager$/);
493+
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
494+
});
495+
496+
it('CONTROL: the base’s flow before Customize says nothing of the kind', async () => {
497+
mount({ listedPair: PAIR });
498+
await openFlow();
499+
await within(header()).findByTestId('studio-customize', undefined, { timeout: 8000 });
500+
expect(savedTo()).toBeNull();
501+
expect(header().querySelector('[data-slot="item-note"]')).toBeNull();
502+
});
503+
504+
it('CONTROL: a writable package’s flow says nothing of the kind', async () => {
505+
mount({ readOnly: false });
506+
await openFlow();
507+
expect(savedTo()).toBeNull();
508+
});
509+
});

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
* - a refusal (the ensure door's, the save door's) is shown as sent;
2727
* - walled: Add field draws disabled in the console's sentence, and nothing
2828
* is asked or written;
29-
* - CONTROL: a writable package's add-field and inspector are unchanged.
29+
* - CONTROL: a writable package's add-field and inspector are unchanged;
30+
* - objectstack#29691: once the base object carries the pair's extension
31+
* (published, or the first add), the object header says where it is saved
32+
* ("Saved to HotCRM’s customizations"); before that, and on a writable
33+
* package, it says nothing of the kind.
3034
*/
3135

3236
import '@testing-library/jest-dom/vitest';
@@ -442,3 +446,45 @@ describe('A Chinese label names a base object\'s extension field by pinyin, suff
442446
});
443447
});
444448
});
449+
450+
describe('the object header says where a base object’s extension is saved (objectstack#29691)', () => {
451+
const SAVED_TO = 'Saved to HotCRM’s customizations';
452+
const savedTo = () => within(screen.getByTestId('data-object-header')).queryByTestId('data-object-saved-to');
453+
454+
it('the first add makes the extension, and the header says it is saved to the base’s customizations', async () => {
455+
await renderBase();
456+
await openForm();
457+
expect(savedTo()).toBeNull();
458+
await addFromFormToolbar('Region');
459+
await waitFor(() => expect(extensionSaves()).toHaveLength(1), { timeout: 8000 });
460+
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
461+
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
462+
});
463+
464+
it('a base object the pair’s published extension already extends says it on open', async () => {
465+
const region = { type: 'text', label: 'Region' };
466+
srv.served = { ...baseServed(), fields: { ...baseServed().fields, region__c: region } };
467+
srv.publishedExt = { extend: OBJECT, fields: { region__c: region }, _packageId: PAIR };
468+
await renderBase();
469+
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
470+
});
471+
472+
it('CONTROL: a base object with no extension says nothing of the kind', async () => {
473+
await renderBase();
474+
await openForm();
475+
// The extension has been read: its Add field is offered.
476+
await screen.findByTestId('data-form-add-extension-field', undefined, { timeout: 8000 });
477+
expect(savedTo()).toBeNull();
478+
expect(screen.getByTestId('data-object-header').querySelector('[data-slot="item-note"]')).toBeNull();
479+
});
480+
481+
it('CONTROL: an object of a writable package says nothing of the kind', async () => {
482+
render(
483+
<MemoryRouter initialEntries={['/studio/com.example.showcase/data']}>
484+
<DataPillar packageId="com.example.showcase" />
485+
</MemoryRouter>,
486+
);
487+
await waitFor(() => expect(screen.getByTestId('records-panel')).toBeInTheDocument(), { timeout: 8000 });
488+
expect(savedTo()).toBeNull();
489+
});
490+
});

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,21 @@
2727
* - CONTROL: a base with no pair keeps the read-only Publish and reviews
2828
* the base, as before;
2929
* - objectstack#29691: reviewing the pair, the panel is handed the base's
30-
* label for its "saved to" sentence, and nothing without a pair.
30+
* label for its "saved to" sentence, and nothing without a pair;
31+
* - objectstack#29691: after Customize, the item header says the same
32+
* sentence (the REAL `SavedToCustomizationNote`; the stubbed panel draws
33+
* its line with the REAL `savedToCustomizationText` from the label the
34+
* host hands it, so the two lines read the same), and nothing before.
35+
* Both come from `preview/savedToCustomization`, which no stub replaces.
3136
*/
3237

3338
import '@testing-library/jest-dom/vitest';
3439
import * as React from 'react';
3540
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
3641
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
3742
import { MemoryRouter, Route, Routes } from 'react-router-dom';
43+
import { useObjectTranslation } from '@object-ui/i18n';
44+
import { savedToCustomizationText } from '../../preview/savedToCustomization';
3845

3946
const BASE = 'com.example.hotcrm';
4047
const PAIR = 'com.example.hotcrm.custom';
@@ -79,6 +86,12 @@ vi.mock('sonner', () => ({
7986
vi.mock('../../components/SuggestedBindingsPanel', () => ({ SuggestedBindingsPanel: () => null }));
8087
vi.mock('../metadata-admin/AccessExplainPanel', () => ({ AccessExplainPanel: () => null }));
8188
vi.mock('./StudioAiCopilot', () => ({ StudioChatDock: () => null }));
89+
/** objectstack#29691 — the sheet's "saved to" line, in the sheet's own words. */
90+
function SheetSavedTo({ baseLabel }: { baseLabel: string }) {
91+
const { t } = useObjectTranslation();
92+
return <p data-testid="sheet-saved-to">{savedToCustomizationText(t, baseLabel)}</p>;
93+
}
94+
8295
vi.mock('../../preview/DraftChangesPanel', () => ({
8396
DraftChangesPanel: ({
8497
packageId,
@@ -90,6 +103,7 @@ vi.mock('../../preview/DraftChangesPanel', () => ({
90103
customizationOf?: string;
91104
}) => (
92105
<div data-testid="changes-panel" data-package={packageId ?? ''} data-customization-of={customizationOf ?? ''}>
106+
{customizationOf && <SheetSavedTo baseLabel={customizationOf} />}
93107
{onPublish && (
94108
<button type="button" data-testid="publish-all-drafts" onClick={() => void onPublish()}>
95109
publish
@@ -337,3 +351,34 @@ describe('the wall is read from what the server publishes (objectstack#28562)',
337351
expect(within(header()).queryByTestId('studio-customize-blocked')).toBeNull();
338352
});
339353
});
354+
355+
describe('the item header says where the customization is saved (objectstack#29691)', () => {
356+
const SAVED_TO = 'Saved to HotCRM’s customizations';
357+
const savedTo = () => within(header()).queryByTestId('if-item-saved-to');
358+
359+
it('after Customize the header says it, and after the first edit it still does, in the sheet’s own words', async () => {
360+
renderWorkbench();
361+
await openPipeline();
362+
fireEvent.click(await within(header()).findByTestId('studio-customize', undefined, { timeout: 8000 }));
363+
await waitFor(() => expect(savedTo()).toHaveTextContent(SAVED_TO), { timeout: 8000 });
364+
// On its own line under the title row, never in it (the fold measures the row).
365+
expect(savedTo()!.closest('[data-slot="item-note"]')).not.toBeNull();
366+
expect(within(header()).getByRole('heading', { level: 1 }).parentElement).not.toContainElement(savedTo());
367+
368+
fireEvent.click(screen.getByRole('button', { name: 'patch the board' }));
369+
await waitFor(() => expect(srv.saves).toHaveLength(1), { timeout: 8000 });
370+
await waitFor(() => expect(panelPackage()).toBe(PAIR), { timeout: 8000 });
371+
expect(savedTo()).toHaveTextContent(SAVED_TO);
372+
// The header line and the sheet line read the same.
373+
expect(screen.getByTestId('sheet-saved-to').textContent).toBe(savedTo()!.textContent);
374+
});
375+
376+
it('CONTROL: before Customize the header says nothing of the kind', async () => {
377+
renderWorkbench();
378+
await openPipeline();
379+
await within(header()).findByTestId('studio-customize', undefined, { timeout: 8000 });
380+
expect(savedTo()).toBeNull();
381+
expect(header().querySelector('[data-slot="item-note"]')).toBeNull();
382+
expect(screen.queryByTestId('sheet-saved-to')).toBeNull();
383+
});
384+
});

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ import {
441441
type DraftChangesPosture,
442442
type DraftPublishOptions,
443443
} from '../../preview/DraftChangesPanel.js';
444+
import { SavedToCustomizationNote } from '../../preview/savedToCustomization.js';
444445
import { toast } from 'sonner';
445446

446447
/** The batch door's `backfillApplied` receipt, by reference to what the client returns (objectui#12929). */
@@ -6300,12 +6301,18 @@ export function InterfacesPillar({
63006301
)}
63016302
</div>
63026303
) : null;
6304+
// objectstack#29691 (ADR-0146 D3) — an item the pair adds, overrides or is
6305+
// unlocked to customize saves to the base's pair: the header says where,
6306+
// in the pending-changes sheet's own words.
6307+
const ifSavedTo =
6308+
customization !== null && (currentAddedIn !== null || headerCustomize?.inPair === true || headerUnlocked);
63036309
const ifHeaderEl = (
63046310
<StudioItemHeader
63056311
testId="if-item-header"
63066312
// objectstack#29693 — on a narrow row the chips and the item tools fold
63076313
// into the ⋯ menu before the title drops below its readable width.
63086314
foldMenu={itemMoreEl ? 'pillar' : { label: t('engine.studio.if.more', locale), testId: 'if-item-header-more' }}
6315+
note={ifSavedTo ? <SavedToCustomizationNote baseLabel={customizeBaseLabel} testId="if-item-saved-to" /> : undefined}
63096316
leading={
63106317
<button
63116318
type="button"
@@ -8953,6 +8960,19 @@ export function DataPillar({
89538960
// objectstack#29693 — the header has no ⋯ of its own (objectui#12511),
89548961
// so on a narrow row it draws one for the chips and the item tools.
89558962
foldMenu={{ label: t('engine.studio.if.more', locale), testId: 'data-object-header-more' }}
8963+
// objectstack#29691 (ADR-0146 D3) — an object the pair adds, or a
8964+
// base object the pair extends, saves to the base's pair. Named
8965+
// by the base's label: `packageId` names the pair for a pair's object.
8966+
note={
8967+
customization !== null &&
8968+
(!!current?.addedIn ||
8969+
(extOpen !== null && (extOpen.published.size > 0 || extHasDraft || extDirty))) ? (
8970+
<SavedToCustomizationNote
8971+
baseLabel={customization.baseLabel}
8972+
testId="data-object-saved-to"
8973+
/>
8974+
) : undefined
8975+
}
89568976
leading={
89578977
<button
89588978
type="button"
@@ -11260,6 +11280,13 @@ export function AutomationsPillar({
1126011280
foldMenu={
1126111281
flowMoreShown ? 'pillar' : { label: t('engine.studio.if.more', locale), testId: 'auto-flow-header-more' }
1126211282
}
11283+
// objectstack#29691 (ADR-0146 D3) — a flow the pair adds, overrides
11284+
// or is unlocked to customize saves to the base's pair.
11285+
note={
11286+
customization !== null && (!!current?.addedIn || flowInPair || flowUnlocked) ? (
11287+
<SavedToCustomizationNote baseLabel={customizeBaseLabel} testId="auto-flow-saved-to" />
11288+
) : undefined
11289+
}
1126311290
leading={
1126411291
<button
1126511292
type="button"

0 commit comments

Comments
 (0)