Skip to content

Commit 7f328cd

Browse files
feat(app-shell): fold the .custom pair's landing card under its base and name the base on the pending-changes sheet (#29723)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbUbQXNJDPax1UE5upP9BnZj)_ Part of #29691 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:Studio 首页「我的软件包」不再给 `BASE.custom` 定制包单独一张卡,它折叠在基座卡下(与软件包切换器同一规则);基座卡保留「已定制 n 项」及其链接。待发布变更面板在审阅定制包草稿时,标题下多一句「保存到 基座名 的定制」。 - 为什么改:定制一个只读软件包后,首页冒出一张用户从没建过的「可写」包卡,而面板只说「本软件包」,用户不知道改动存到了哪里(ADR-0146 D3)。 - 风险与回滚:仅 UI 与一条新文案键;定制包的导出与删除仍在 Studio「概览 → 软件包」页(已实测)。回滚即还原本 PR,无数据或协议变化。 - 先看:`BuilderLanding.tsx` 的折叠(`foldCustomizationPairs`);`DraftChangesPanel.tsx` 的 `customizationOf` 一行;`StudioDesignSurface.tsx` 只在 `pairId` 存在时传入基座名。 - 两分钟验证:在 showcase 定制任意一项后打开 `/_console/studio`,「我的软件包」里不再有 `com.example.showcase.custom` 卡,只读区的 Showcase 卡写着「已定制 1 项」;点 Publish 打开面板,标题下是「保存到 ObjectStack Showcase 的定制」。 - 需要你做的:无,席位落地。 ## What changes - **Landing (ADR-0146 D3).** `BuilderLanding` folds each `BASE.custom` pair under its base with the switcher's own `foldCustomizationPairs` before it splits writable from read-only. A pair whose base is listed draws no card in either section. The base's card keeps its "n customized" line, and that line's link into the pair. A pair whose base is not listed keeps its card, the switcher's rule too. - **Pending-changes sheet.** `DraftChangesPanel` takes an optional `customizationOf` (the base's label) and, when set, says under its title "Saved to BASE's customizations" (zh 保存到 BASE 的定制). The sentence is one exported helper, `savedToCustomizationText`, so the item header can say the same words in the follow-up PR. `StudioDesignSurface` passes the base label only while it reviews a pair's drafts (`pairId` set), and nothing for a writable package or a base with no pair. - **i18n.** One new key, `preview.changes.savedToCustomization`, in all ten `@object-ui/i18n` locale packs. Nothing is removed or renamed. - **Not changed:** the publish toast (the card's direction names only the item header and the sheet), and the item header, which waits for #29693 and lands in a second PR. ## Contract review - **What widens:** the published `@object-ui/i18n` locale packs gain one leaf, `preview.changes.savedToCustomization`, in each of the ten packs, and `@object-ui/app-shell` gains an optional `DraftChangesPanel` prop (`customizationOf`) and one exported helper (`savedToCustomizationText`). - **What narrows:** nothing. No key, prop, export or accepted value is removed or tightened. Metadata authors see no change (`Metadata-protocol: none`). ## Tests - `BuilderLanding.customizationCounts-28566.test.tsx`: a new describe over the real landing. A read-only base and its writable pair draw one card, the base's, with its customized count and the pair link, and the pair's name and id appear on no card. A read-only pair folds under its listed base too. CONTROL: a writable package that is no pair keeps its card, and a pair whose base is not listed keeps its card. The existing "no line for a pair" case now finds the orphan pair's card and no card for the folded one. - `DraftChangesPanel.test.tsx`: a new describe that renders the real sheet through the real i18next hook and the shipped packs. With `customizationOf="HotCRM"` it reads 保存到 HotCRM 的定制 in zh and "Saved to HotCRM’s customizations" in en. CONTROL: without it, no sentence. - `StudioDesignSurface.customizePublish-28562.test.tsx` (the real surface): reviewing a listed pair, and after the first edit makes the pair, the sheet is handed the base label `HotCRM`. CONTROL: a base with no pair hands it nothing. ## Acceptance notes - **Where the pair's export and delete live.** They were never on the landing card, which offered only Open and Duplicate. They are on Studio, Overview → Packages (`nav_packages` → `/apps/studio/component/developer/packages`, the `developer:packages` page). That page lists every served package with no pair fold. The pair's row opens a detail sheet with Export (every row, `GET /api/v1/packages/:id/export`) and Delete package (writable rows, `DELETE /api/v1/packages/:id`). Measured live on a fresh showcase (`pnpm dev -- --fresh`, branch head `d47495aa`): `POST /api/v1/packages/com.example.showcase/ensure-customization` answered 201 and created `com.example.showcase.custom`. `GET /api/v1/packages` then served it as its own row (`writable: true`, `source: authored`), the row the Packages page draws. `GET /api/v1/packages/com.example.showcase.custom/export` answered 200 with the pair's manifest, and `DELETE /api/v1/packages/com.example.showcase.custom` answered 200, after which the list held only `com.example.showcase`. Those are the doors the sheet's Export and Delete package buttons call (`PackagesPage`: Export on every row, Delete package on writable rows); the buttons themselves were read from code, not clicked. - **The review's claim that the pair card's link returns to the base holds.** The pair card opened `/studio/PAIR/data`, and the `StudioDesignSurface` wrapper sends every pillar URL of a pair to the base's surface. `StudioDesignSurface.oneSurface-28572` already pins that. The base card's customized link takes the same path, unchanged. ## Verification <details><summary>Tests and gates (final head 62ad4a4; sweep on d47495a, and the merge of main brought no file under ui/)</summary> - **Red on main, green here.** The three touched test files with `origin/main` `1c1a5d40`'s `BuilderLanding.tsx`, `DraftChangesPanel.tsx` and `StudioDesignSurface.tsx` (each blob-identical to main, restored to `HEAD` after, `git diff HEAD` empty): 7 failed, 24 passed. The 7 are the new pins; the CONTROL cases stay green on main. On the branch: Test Files 3 passed, Tests 31 passed, on `d47495aa` and again on `62ad4a49`. - **Sweep** (`--maxWorkers=2`, locked): every ui test file naming `BuilderLanding` or `DraftChangesPanel` plus every test reading the locale packs or `preview.changes`, 254 files in six batches, all green (346 + 425 + 554 + 289 + 519 + 677 tests), and `packages/i18n/` whole: 95 files, 1443 passed, 13 skipped. - **Type check:** `turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n`: 66 of 66 tasks green. - **eslint** on the touched source and test files: 0 errors, 8 warnings. One is react-refresh's only-export-components warning on the new `savedToCustomizationText` export, the warning the file's three other exported helpers already carry. The other 7 are on lines this PR does not touch. - **Derived gates** (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`): 35 commands, each exit 0, 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/` (170 files passed, 2 skipped). - **Not run locally:** the full Test UI shards, which CI's Test UI runs. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTWKHRdA5msPPhwmw4)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent fd43dc2 commit 7f328cd

17 files changed

Lines changed: 186 additions & 13 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(app-shell): the Studio landing gives a base's `.custom` pair no card of its own, and the pending-changes sheet says where a customization is saved
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
ADR-0146 D3 folds a base's `<base>.custom` pair under its base. The Studio landing now does the same: a pair whose base is listed draws no card under "My packages" (or with the read-only packages). The base's card keeps its "n customized" line, and that line still links into the pair. A pair whose base is not listed keeps its card. The pair's Export and Delete stay on Studio's Overview → Packages page, which lists every package.
12+
13+
When Studio reviews a read-only base's pair, the pending-changes sheet now says, under its title, "Saved to BASE's customizations" (zh: 保存到 BASE 的定制). `@object-ui/i18n` adds one key, `preview.changes.savedToCustomization`, in all ten locale packs. `DraftChangesPanel` takes an optional `customizationOf` prop, the base's label, and exports `savedToCustomizationText` so other surfaces can say the same sentence. Nothing is removed or narrowed.

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,10 +1328,30 @@ export interface DraftChangesPanelProps {
13281328
* nothing to say.
13291329
*/
13301330
renderEntryNote?: (entry: DraftChangeEntry) => ReactNode;
1331+
/**
1332+
* objectstack ADR-0146 D3 — the label of the read-only base whose
1333+
* `<base>.custom` pair these drafts are saved to. When set, the sheet says
1334+
* so under its title ({@link savedToCustomizationText}); the host passes it
1335+
* only while it reviews a pair's drafts.
1336+
*/
1337+
customizationOf?: string;
13311338
}
13321339

13331340
export type DraftChangesPosture = 'review' | 'confirm';
13341341

1342+
/**
1343+
* objectstack ADR-0146 D3 — the one sentence that names where a customization
1344+
* of a read-only base is saved: "Saved to BASE's customizations". The
1345+
* pending-changes sheet draws it, and the item header reuses it, so both say
1346+
* the same words.
1347+
*/
1348+
export function savedToCustomizationText(t: Translate, baseLabel: string): string {
1349+
return t('preview.changes.savedToCustomization', {
1350+
package: baseLabel,
1351+
defaultValue: 'Saved to {{package}}’s customizations',
1352+
});
1353+
}
1354+
13351355
export function DraftChangesPanel({
13361356
open,
13371357
onOpenChange,
@@ -1341,6 +1361,7 @@ export function DraftChangesPanel({
13411361
publishing = false,
13421362
posture = 'review',
13431363
renderEntryNote,
1364+
customizationOf,
13441365
}: DraftChangesPanelProps) {
13451366
const { t, language } = useObjectTranslation();
13461367
const [entries, setEntries] = useState<DraftChangeEntry[] | null>(null);
@@ -1608,6 +1629,11 @@ export function DraftChangesPanel({
16081629
defaultValue: 'What publishing will change. New items are added; updates overwrite the live version.',
16091630
})}
16101631
</SheetDescription>
1632+
{customizationOf ? (
1633+
<p data-testid="draft-changes-saved-to" className="text-xs font-medium text-foreground">
1634+
{savedToCustomizationText(t, customizationOf)}
1635+
</p>
1636+
) : null}
16111637
</SheetHeader>
16121638
{posture === 'confirm' && entries && entries.length > 0 && !error ? (
16131639
// objectui#12517 — what the publish carries, counted off the same

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

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,26 @@ import * as React from 'react';
1111
import { afterEach, describe, expect, it, vi } from 'vitest';
1212
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
1313

14-
vi.mock('@object-ui/i18n', async (importOriginal) => ({
15-
...(await importOriginal<Record<string, unknown>>()),
16-
useObjectTranslation: () => ({
17-
t: (_k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? _k,
18-
}),
19-
}));
14+
// `t()` answers each key's default text; a describe that sets `i18nMode.real`
15+
// reads the shipped packs through the real hook instead.
16+
const i18nMode = vi.hoisted(() => ({ real: false }));
17+
vi.mock('@object-ui/i18n', async (importOriginal) => {
18+
const actual = await importOriginal<typeof import('@object-ui/i18n')>();
19+
return {
20+
...actual,
21+
useObjectTranslation: (ns?: string) =>
22+
i18nMode.real
23+
? actual.useObjectTranslation(ns)
24+
: { t: (_k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? _k },
25+
};
26+
});
2027

28+
import { createI18n, I18nProvider } from '@object-ui/i18n';
2129
import { DraftChangesPanel, computeChangeDetail } from '../DraftChangesPanel';
2230

2331
afterEach(() => {
2432
vi.restoreAllMocks();
33+
i18nMode.real = false;
2534
});
2635

2736
/* ─────────────── computeChangeDetail (pure) ─────────────── */
@@ -249,3 +258,40 @@ describe('DraftChangesPanel — /meta routes are addressed in the singular', ()
249258
expect(screen.getByRole('heading', { level: 4 }).textContent).toBe('{{count}} objects');
250259
});
251260
});
261+
262+
/* ─────────────── where a customization is saved (ADR-0146 D3) ─────────────── */
263+
264+
/**
265+
* objectstack#29691 — reviewing a read-only base's `.custom` pair, the sheet
266+
* says where the changes are saved, in the one sentence the item header
267+
* shares: 保存到 BASE 的定制. Rendered through the real i18next hook and the
268+
* shipped packs.
269+
*/
270+
describe('DraftChangesPanel — names the base whose customizations it saves (objectstack#29691)', () => {
271+
function renderIn(lang: 'en' | 'zh', extra: Partial<React.ComponentProps<typeof DraftChangesPanel>> = {}) {
272+
i18nMode.real = true;
273+
mockRoutes();
274+
const i18n = createI18n({ defaultLanguage: lang, detectBrowserLanguage: false });
275+
return render(
276+
<I18nProvider instance={i18n} persistLanguage={false}>
277+
<DraftChangesPanel open onOpenChange={() => {}} packageId="com.x" {...extra} />
278+
</I18nProvider>,
279+
);
280+
}
281+
282+
it('says 保存到 HotCRM 的定制 under the title for a pair, in zh', async () => {
283+
renderIn('zh', { customizationOf: 'HotCRM' });
284+
await waitFor(() => expect(screen.getByTestId('draft-changes-saved-to')).toHaveTextContent(/^保存到 HotCRM 的定制$/));
285+
});
286+
287+
it('says “Saved to HotCRM’s customizations” in en', async () => {
288+
renderIn('en', { customizationOf: 'HotCRM' });
289+
expect(await screen.findByTestId('draft-changes-saved-to')).toHaveTextContent(/^Saved to HotCRM’s customizations$/);
290+
});
291+
292+
it('CONTROL: a writable package’s own drafts carry no such sentence', async () => {
293+
renderIn('en');
294+
await waitFor(() => expect(screen.getByText('ticket')).toBeInTheDocument());
295+
expect(screen.queryByTestId('draft-changes-saved-to')).not.toBeInTheDocument();
296+
});
297+
});

‎ui/packages/app-shell/src/views/studio-design/BuilderLanding.customizationCounts-28566.test.tsx‎

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,15 @@ describe('Studio landing: a read-only base says "n customized, m switched off" (
185185
const { requested } = stubWire({ 'com.example.showcase': served('com.example.showcase', 1, 0, 0) });
186186
renderLanding();
187187

188-
await screen.findByText('Showcase pair');
188+
await screen.findByText('HotCRM customizations');
189189
await waitFor(() =>
190190
expect(within(cardOf('Showcase')).getByTestId('studio-landing-customizations')).toHaveAttribute('data-state', 'read'),
191191
);
192192
expect(within(cardOf('Acme')).queryByTestId('studio-landing-customizations')).not.toBeInTheDocument();
193+
// A pair whose base is not listed keeps its card, with no line.
193194
expect(within(cardOf('HotCRM customizations')).queryByTestId('studio-landing-customizations')).not.toBeInTheDocument();
194-
expect(within(cardOf('Showcase pair')).queryByTestId('studio-landing-customizations')).not.toBeInTheDocument();
195+
// A pair whose base is listed has no card at all (#29691's describe below).
196+
expect(screen.queryByText('Showcase pair')).not.toBeInTheDocument();
195197
expect(customizationRequests(requested)).toEqual(['/api/v1/packages/com.example.showcase/customizations']);
196198
});
197199

@@ -210,3 +212,51 @@ describe('Studio landing: a read-only base says "n customized, m switched off" (
210212
}
211213
});
212214
});
215+
216+
/**
217+
* objectstack#29691 (ADR-0146 D3) — the landing gives a base's `.custom` pair
218+
* no card of its own: the pair is folded under its base, as the package
219+
* switcher folds it, and the base's card carries it through its customized
220+
* line and link. The pair's export and delete stay on the Packages page.
221+
*/
222+
describe('Studio landing: a base’s .custom pair has no card of its own (ADR-0146 D3)', () => {
223+
/** Every package card the landing draws, by the id line each card shows. */
224+
const cardIds = () =>
225+
Array.from(document.querySelectorAll('.rounded-lg.border .font-mono')).map((el) => el.textContent);
226+
227+
it('draws one card for a base and its writable pair: the base’s, with its customized count and the pair link', async () => {
228+
packages.list = [HOTCRM, HOTCRM_PAIR, WRITABLE];
229+
stubWire({ hotcrm: served('hotcrm', 2, 1, 0) });
230+
renderLanding();
231+
232+
const line = await within(await waitFor(() => cardOf('HotCRM'))).findByTestId('studio-landing-customizations');
233+
await waitFor(() => expect(line).toHaveAttribute('data-state', 'read'));
234+
expect(within(line).getByTestId('studio-landing-customizations-pair')).toHaveTextContent(customizedText(3));
235+
expect(within(line).getByTestId('studio-landing-customizations-pair')).toHaveAttribute('href', '/studio/hotcrm.custom/data');
236+
237+
// The pair draws no card, under 我的软件包 or anywhere else.
238+
expect(screen.queryByText('HotCRM customizations')).not.toBeInTheDocument();
239+
expect(cardIds()).not.toContain('hotcrm.custom');
240+
// CONTROL: a writable package that is no pair keeps its card.
241+
expect(cardIds()).toEqual(['com.acme.app', 'hotcrm']);
242+
});
243+
244+
it('folds a read-only pair under its listed base too', async () => {
245+
packages.list = [SHOWCASE, READONLY_PAIR];
246+
stubWire({ 'com.example.showcase': served('com.example.showcase', 1, 0, 0) });
247+
renderLanding();
248+
249+
await waitFor(() => cardOf('Showcase'));
250+
expect(screen.queryByText('Showcase pair')).not.toBeInTheDocument();
251+
expect(cardIds()).toEqual(['com.example.showcase']);
252+
});
253+
254+
it('keeps the card of a pair whose base is not listed', async () => {
255+
packages.list = [HOTCRM_PAIR];
256+
stubWire({});
257+
renderLanding();
258+
259+
await waitFor(() => cardOf('HotCRM customizations'));
260+
expect(cardIds()).toEqual(['hotcrm.custom']);
261+
});
262+
});

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { PackageIdInput } from './PackageIdInput.js';
3636
import { studioOrgScopePath } from './studioScope.js';
3737
import { STUDIO_ALL_PACKAGES_SEGMENT, studioMetadataAreaPath } from './metadataArea.js';
3838
import { STUDIO_NEW_PARAM, STUDIO_NEW_PACKAGE_VALUE } from './studioNewPackage.js';
39+
import { foldCustomizationPairs } from './StudioRailCustomizations.js';
3940

4041
/**
4142
* objectui#11808 — where a read-only package's card sends the author beyond
@@ -117,8 +118,15 @@ export function BuilderLanding(): React.ReactElement {
117118
.filter((item): item is RecentNamedItem => item.type === 'package')
118119
.slice(0, RECENT_PACKAGES_SHOWN);
119120

120-
const writable = pkgs?.filter((p) => p.writable) ?? [];
121-
const readonly = pkgs?.filter((p) => !p.writable) ?? [];
121+
// ADR-0146 D3 — a base's `.custom` pair has no card of its own: it is
122+
// folded under its base, the way the package switcher folds it
123+
// (`foldCustomizationPairs`). The base's card carries the pair through its
124+
// customized line and link, and the pair's export and delete stay on the
125+
// Packages page, which lists every package. A pair whose base is not listed
126+
// keeps its card.
127+
const cards = pkgs === null ? [] : foldCustomizationPairs(pkgs).rows;
128+
const writable = cards.filter((p) => p.writable);
129+
const readonly = cards.filter((p) => !p.writable);
122130

123131
// ADR-0146 D3 — each read-only BASE's card carries "n customized, m switched
124132
// off", the server's counts (`GET /packages/:id/customizations`), one read

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
* - a walled admin (a group posture, not the platform operator) sees
2626
* Customize disabled, and the platform operator does not;
2727
* - CONTROL: a base with no pair keeps the read-only Publish and reviews
28-
* the base, as before.
28+
* the base, as before;
29+
* - objectstack#29691: reviewing the pair, the panel is handed the base's
30+
* label for its "saved to" sentence, and nothing without a pair.
2931
*/
3032

3133
import '@testing-library/jest-dom/vitest';
@@ -78,8 +80,16 @@ vi.mock('../../components/SuggestedBindingsPanel', () => ({ SuggestedBindingsPan
7880
vi.mock('../metadata-admin/AccessExplainPanel', () => ({ AccessExplainPanel: () => null }));
7981
vi.mock('./StudioAiCopilot', () => ({ StudioChatDock: () => null }));
8082
vi.mock('../../preview/DraftChangesPanel', () => ({
81-
DraftChangesPanel: ({ packageId, onPublish }: { packageId: string | null; onPublish?: () => void | Promise<void> }) => (
82-
<div data-testid="changes-panel" data-package={packageId ?? ''}>
83+
DraftChangesPanel: ({
84+
packageId,
85+
onPublish,
86+
customizationOf,
87+
}: {
88+
packageId: string | null;
89+
onPublish?: () => void | Promise<void>;
90+
customizationOf?: string;
91+
}) => (
92+
<div data-testid="changes-panel" data-package={packageId ?? ''} data-customization-of={customizationOf ?? ''}>
8393
{onPublish && (
8494
<button type="button" data-testid="publish-all-drafts" onClick={() => void onPublish()}>
8595
publish
@@ -233,6 +243,8 @@ function renderWorkbench() {
233243

234244
const publishButton = () => screen.getByRole('button', { name: /^Publish/ });
235245
const panelPackage = () => screen.getByTestId('changes-panel').getAttribute('data-package');
246+
/** The base label the host hands the sheet's "saved to" sentence (objectstack#29691); '' when none. */
247+
const panelCustomizationOf = () => screen.getByTestId('changes-panel').getAttribute('data-customization-of');
236248
const header = () => screen.getByTestId('if-item-header');
237249

238250
async function openPipeline(): Promise<void> {
@@ -246,6 +258,8 @@ describe('a read-only base with its pair listed: Pending, the panel and Publish
246258
renderWorkbench();
247259

248260
await waitFor(() => expect(panelPackage()).toBe(PAIR), { timeout: 8000 });
261+
// objectstack#29691 — the sheet names the base the pair's drafts are saved for.
262+
expect(panelCustomizationOf()).toBe('HotCRM');
249263
await waitFor(() => expect(srv.reads).toContain(`/api/v1/meta/_drafts?packageId=${encodeURIComponent(PAIR)}`));
250264
await waitFor(() => expect(publishButton()).toBeEnabled(), { timeout: 8000 });
251265
expect(publishButton().parentElement).not.toHaveAttribute('data-disabled-reason');
@@ -259,6 +273,7 @@ describe('a read-only base with its pair listed: Pending, the panel and Publish
259273
await waitFor(() => expect(publishButton().parentElement).toHaveAttribute('data-disabled-reason'), { timeout: 8000 });
260274
expect(publishButton()).toBeDisabled();
261275
expect(panelPackage()).toBe(BASE);
276+
expect(panelCustomizationOf()).toBe('');
262277
expect(screen.queryByTestId('publish-all-drafts')).toBeNull();
263278
});
264279
});
@@ -278,6 +293,7 @@ describe('end to end: the first edit makes the pair, and Pending and Publish fol
278293
expect(saved!.body).toStrictEqual({ ...PIPELINE, label: 'Pipeline (ours)', overrides: { package: BASE, basedOn: '4.0.0' } });
279294

280295
await waitFor(() => expect(panelPackage()).toBe(PAIR), { timeout: 8000 });
296+
expect(panelCustomizationOf()).toBe('HotCRM');
281297
await waitFor(() => expect(publishButton()).toBeEnabled(), { timeout: 8000 });
282298
fireEvent.click(screen.getByTestId('publish-all-drafts'));
283299
await waitFor(() => expect(srv.publishes).toEqual([PAIR]));

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2857,6 +2857,9 @@ function StudioDesignSurfaceBody({ aiSlot, area }: StudioDesignSurfaceProps): Re
28572857
publishing={publishing}
28582858
posture={changesPosture}
28592859
renderEntryNote={renderPublishRefusal}
2860+
// ADR-0146 D3 — reviewing the pair's drafts, the sheet names the
2861+
// base they are saved for.
2862+
customizationOf={pairId !== null ? baseLabel : undefined}
28602863
/>
28612864

28622865
<CreateItemDialog

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4887,6 +4887,7 @@ const ar = {
48874887
changes: {
48884888
title: 'التغييرات المعلّقة',
48894889
description: 'ما الذي سيغيّره النشر. تُضاف العناصر الجديدة؛ وتستبدل التحديثات النسخة المنشورة.',
4890+
savedToCustomization: 'محفوظ في تخصيصات {{package}}',
48904891
loading: 'جارٍ تحميل التغييرات المعلّقة…',
48914892
loadFailed: 'تعذّر تحميل التغييرات المعلّقة:',
48924893
empty: 'لا شيء معلّق — نُشرت كل المسودات.',

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4490,6 +4490,7 @@ const de = {
44904490
changes: {
44914491
title: 'Ausstehende Änderungen',
44924492
description: 'Was die Veröffentlichung ändert. Neue Elemente werden hinzugefügt; Aktualisierungen überschreiben die Live-Version.',
4493+
savedToCustomization: 'Gespeichert in den Anpassungen von {{package}}',
44934494
loading: 'Ausstehende Änderungen werden geladen…',
44944495
loadFailed: 'Ausstehende Änderungen konnten nicht geladen werden:',
44954496
empty: 'Nichts ausstehend — alle Entwürfe wurden veröffentlicht.',

‎ui/packages/i18n/src/locales/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3953,6 +3953,8 @@ const en = {
39533953
changes: {
39543954
title: 'Pending changes',
39553955
description: 'What publishing will change. New items are added; updates overwrite the live version.',
3956+
// ADR-0146 D3 — where a customization of a read-only base is saved: its `.custom` pair.
3957+
savedToCustomization: 'Saved to {{package}}’s customizations',
39563958
loading: 'Loading pending changes…',
39573959
loadFailed: 'Could not load pending changes:',
39583960
empty: 'Nothing pending — every draft has been published.',

0 commit comments

Comments
 (0)