Skip to content

Commit 644883e

Browse files
fix(studio): after Restore package original the rail's customized mark and "only customizations" count refresh without a reload (#30381)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg4bKKModkHh9FMR76oxRgy1)_ domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2 Fixes #30238 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Studio 读取「本基础包有哪些定制」(`GET /api/v1/packages/:id/customizations`)的时机,从「只在发布后」改为「每次元数据刷新脉冲」;恢复原版本来就会发这个脉冲,所以恢复后侧栏的定制标记和「仅看定制(n)」立即更新,不用刷新页面。 - 为什么改:管理员在 Studio 里对已安装应用的仪表盘、报表、视图、列表视图或页面按「恢复原版」后,内容回到出厂版,但侧栏还标着已定制、「仅看定制(1)」还在,看不出恢复是否生效,可能重复恢复或误以为定制仍在。 - 风险与回滚:只改 `StudioDesignSurface.tsx` 中一处读取的触发键(含注释 +13/-3 行),外加一个新测试文件和 changeset;不动 spec、服务端或元数据。发布照样只读一次(发布本来就同时发脉冲);AI 助手发布后也会重读。回滚即 revert 本 PR。 - 先看:`ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx` 约 836–849 行(`customizationsRefresh` 那一段)→ 新测试 `StudioDesignSurface.restoreMarks-30238.test.tsx`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.restoreMarks-30238.test.tsx`,预期 2/2 通过(在 main 源码上两条都失败:恢复后定制读取没有再请求)。 - 需要你做的:无,席位落地。 ## What changes `StudioDesignSurface` reads the base's customizations once and hands them to every pillar as `customization.marks`: every pillar rail's marks and "only customizations" count come from that read. It was keyed on `publishNonce`, so it was read again only after a publish. "Restore package original" (`restoreOriginal` in the Interfaces pillar, the flow restore in Automations, and the bundle item editor's restore) deletes the pair's draft and row, then calls `emitMetadataRefresh()`, but never moves `publishNonce`. So the shipped item read back while the rail kept its mark and its count until a reload. - The read is now keyed on `useMetadataRefreshNonce()`, the assistant bus pulse that the restores already emit. Both publish paths (`setPublishNonce` at the two publish sites) emit the same pulse in the same handler, so a publish still reads once. Keying on both would read twice per publish. A copilot publish from the right dock, which the old key missed, now refreshes the marks too. - No restore path changes, and no new fetch path. The hook (`usePackageCustomizations`) is unchanged. The card's mechanism held as written: `restoreOriginal` moves `servedNonce`, the leaf or list-view or nav reload nonces, and emits the pulse, but no key the customization read listened to. I confirmed it on a second type: a dashboard opened from the navigation, besides the page the card measured. ## Tests - New `ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.restoreMarks-30238.test.tsx`: the REAL `StudioDesignSurface` and metadata client over a fetch stub that serves a read-only base with its pair listed, the pair's rows served over the base's items, the customization read (one override per pair row) and the reset door (`DELETE`, which deletes the pair's row). - A dashboard opened from the navigation: before the restore the rail marks it `customized` and "only customizations" reads 1. After the restore the read is asked again, the mark is gone, and so is the filter. - A page listed under its type, with a second customized dashboard as the CONTROL: after the page's restore the read is asked again, the page's mark is gone, the count reads 1, and the dashboard keeps its mark. - Red first, on the commit before the fix (test only, `3e7cd7f5`): `Tests 2 failed (2)`, both `expected 1 to be greater than 1` (the customization read count did not move after the restore). Green after the fix: `Tests 2 passed (2)`. The red-first run on the parent commit is the ablation leg: the fix is the only source change. - Every app-shell test file that renders the whole `StudioDesignSurface` (60 files): `Test Files 60 passed (60)`, `Tests 372 passed (372)`. The other test files that import the module render single pillars with a host-built `customization`, which this change does not touch. - `pnpm exec turbo run type-check --filter=@object-ui/app-shell --concurrency=2`: `Tasks: 68 successful, 68 total`. - `eslint` on the two changed ui files: 0 errors. `StudioDesignSurface.tsx` has 36 warnings, the same count as on main. The new test file has none. ## Acceptance notes - The card's Expected also names the All metadata tile count. The All metadata area's pages (`DirectoryPage`, `ResourceListPage`) make their own customization read when each one mounts. The area mounts on its tab, and its list and item pages are separate routes, so after a restore made anywhere else they open on a fresh read. This PR does not change them. - The card lists the app's navigation with the types that share the path. The navigation's rail mark reads the served override (`useServedOverride`, keyed on `servedNonce`, which `restoreOriginal` moves), not the customization read, so it already cleared after a restore. The count's navigation term is that same mark. The stale part was the off-navigation items (page, dashboard, report, view) and the nav entries' marks, which this PR fixes. ## Verification <details><summary>Derived gates (dispatch-gates --commands, final tree `bde46d2b`)</summary> - `node scripts/check-adr-0087-registration.mjs --base origin/main` :: exit 0 - `node scripts/check-adr-0087-registration.mjs --self-test` :: exit 0 - `node scripts/check-changeset-no-major.mjs --base origin/main` :: exit 0 - `node scripts/check-closing-keyword-parity.mjs` :: exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0 - `node scripts/check-comment-mask-corpus.mjs` :: exit 0 - `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 0 - `node scripts/check-empty-changeset.mjs --self-test` :: exit 0 - `node scripts/check-sdui-manifest.mjs` :: exit 0 - `node scripts/check-sdui-manifest.mjs --self-test` :: exit 0 - `node scripts/pm/release-rehearsal-clone.mjs --self-test` :: exit 0 - `node scripts/release-pending-publish.mjs --self-test` :: exit 0 - `pnpm --dir ui exec vitest run scripts/` :: exit 0 - `pnpm --filter @objectstack/spec run check:duration-unit-keys` :: exit 0 - `pnpm check:changeset-gate-self-tests` :: exit 0 - `pnpm check:closed-ceremony-text` :: exit 0 - `pnpm check:cross-package-test-inputs` :: exit 0 - `pnpm check:dispatcher-error-vocabulary` :: exit 0 - `pnpm check:driver-memory-census` :: exit 0 - `pnpm check:engine-double-contract` :: exit 0 - `pnpm check:gitlink-declared` :: exit 0 - `pnpm check:nul-bytes` :: exit 0 - `pnpm check:published-files` :: exit 0 - `pnpm check:query-options-erasure` :: exit 0 - `pnpm check:refd-timer-probe` :: exit 0 - `pnpm check:registry-log-declared` :: exit 0 - `pnpm check:rest-log-spy-declared` :: exit 0 - `pnpm check:skill-identifier-liveness` :: exit 0 - `pnpm check:test-source-alias` :: exit 0 - `pnpm check:tier-file-adoption` :: exit 0 - `pnpm check:type-check-coverage` :: exit 0 - `pnpm check:type-check-debt` :: exit 0 - `pnpm check:undeclared-dep-imports` :: exit 0 - `pnpm check:watch-hint-literal` :: exit 0 - `pnpm check:where-matcher` :: exit 0 `node scripts/pm/dispatch-gates.mjs --ran`: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. `check:type-check-debt` ran under the verify lock (VERDICT command-exit 0). `check:sdui-manifest` (standing ui gate) is in the list. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CYbJbcjB64iwnxmHBs)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 75a5c3e commit 644883e

3 files changed

Lines changed: 279 additions & 3 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(studio): after Restore package original, the rail's customized mark and the "only customizations" count read the restored item without a reload (#30238)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** On an installed (read-only) app, restoring a customized dashboard, report, view or page in Studio brought the shipped item back, but the Interfaces rail kept marking it customized and "only customizations" kept counting it until the page was reloaded. The surface read the base's customizations (`GET /api/v1/packages/:id/customizations`) again only after a publish, and a restore deletes the pair's row without publishing.
11+
- **Now.** The surface reads them again on every metadata-refresh pulse, which a restore already sends (as do a publish and a copilot publish), so every pillar rail's marks and "only customizations" count follow the restore. A publish still reads once. The All metadata area's pages make their own read each time one opens, so they already showed the restored state.
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#30238 — after "Restore package original" the Interfaces rail
5+
* reads the restored state without a reload: the item's customized mark and
6+
* the "only customizations" count come from the base's one customization read
7+
* (`GET /api/v1/packages/:id/customizations`), and a restore deletes the row
8+
* that read lists, so the surface reads it again.
9+
*
10+
* The REAL `StudioDesignSurface` and the REAL metadata client over a fetch
11+
* stub that serves the package list (a read-only base with its pair listed),
12+
* the base's items, the pair's served rows over them, the customization read
13+
* (one override per pair row it holds) and the reset door (`DELETE`), which
14+
* deletes the pair's row. Stubbed: the dashboard and page canvases (a label
15+
* each), `DraftChangesPanel` and the session, as the surface's other
16+
* customization suite does.
17+
*
18+
* Pinned, on a dashboard (opened from the navigation) and on a page (listed
19+
* under its type), the path every Interfaces type shares:
20+
* - before the restore the rail marks the item and counts it under "only
21+
* customizations";
22+
* - after it the customization read is asked again, the mark is gone and
23+
* the count with it, with no reload;
24+
* - CONTROL: an item that stays customized keeps its mark and its count.
25+
*/
26+
27+
import '@testing-library/jest-dom/vitest';
28+
import * as React from 'react';
29+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
30+
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
31+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
32+
33+
const BASE = 'com.example.hotcrm';
34+
const PAIR = 'com.example.hotcrm.custom';
35+
const DECLARED = { package: BASE, basedOn: '4.0.0' };
36+
37+
const NAV = [
38+
{ id: 'nav_pipeline', type: 'dashboard', label: 'Pipeline', dashboardName: 'pipeline' },
39+
{ id: 'nav_forecast', type: 'dashboard', label: 'Forecast', dashboardName: 'forecast' },
40+
];
41+
const APP = { name: 'crm_app', label: 'CRM', navigation: NAV };
42+
/** The base's rows, by `type/name`. */
43+
const BASE_ROWS: Record<string, Record<string, unknown>> = {
44+
'dashboard/pipeline': { name: 'pipeline', label: 'Pipeline', widgets: [] },
45+
'dashboard/forecast': { name: 'forecast', label: 'Forecast', widgets: [] },
46+
'page/crm_welcome': { name: 'crm_welcome', label: 'Welcome', type: 'home', kind: 'full', regions: [] },
47+
};
48+
49+
const srv = vi.hoisted(() => ({
50+
/** The pair's ACTIVE rows, by `type/name`: each is served over the base's item and listed as an override. */
51+
pairRows: new Map<string, Record<string, unknown>>(),
52+
customizationReads: 0,
53+
resets: [] as string[],
54+
}));
55+
56+
vi.mock('@object-ui/auth', async (importOriginal) => {
57+
const actual = await importOriginal<typeof import('@object-ui/auth')>();
58+
return {
59+
...actual,
60+
useAuth: () => ({ ...actual.useAuth(), user: null, getAuthConfig: async () => ({ features: {} }) }),
61+
};
62+
});
63+
64+
vi.mock('@object-ui/react', async (importOriginal) => {
65+
const actual = await importOriginal<Record<string, unknown>>();
66+
return { ...actual, useAdapter: () => ({}) };
67+
});
68+
69+
vi.mock('sonner', () => ({
70+
toast: { warning: vi.fn(), error: vi.fn(), success: vi.fn(), info: vi.fn(), message: vi.fn(), dismiss: vi.fn() },
71+
Toaster: () => null,
72+
}));
73+
vi.mock('../../components/SuggestedBindingsPanel', () => ({ SuggestedBindingsPanel: () => null }));
74+
vi.mock('../metadata-admin/AccessExplainPanel', () => ({ AccessExplainPanel: () => null }));
75+
vi.mock('./StudioAiCopilot', () => ({ StudioChatDock: () => null }));
76+
vi.mock('../../preview/DraftChangesPanel', () => ({ DraftChangesPanel: () => null }));
77+
78+
import { StudioDesignSurface } from './StudioDesignSurface';
79+
import { registerMetadataPreview } from '../metadata-admin/preview-registry';
80+
import { tFormat } from '../metadata-admin/i18n';
81+
82+
window.matchMedia = ((query: string) => ({
83+
matches: false,
84+
media: query,
85+
onchange: null,
86+
addEventListener: () => {},
87+
removeEventListener: () => {},
88+
addListener: () => {},
89+
removeListener: () => {},
90+
dispatchEvent: () => false,
91+
})) as unknown as typeof window.matchMedia;
92+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
93+
(globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver ??
94+
class {
95+
observe() {}
96+
unobserve() {}
97+
disconnect() {}
98+
};
99+
100+
function LabelCanvas({ draft }: { draft: Record<string, unknown> }) {
101+
return <p data-testid="canvas-label">{String(draft?.label ?? '')}</p>;
102+
}
103+
104+
function json(body: unknown, status = 200): Response {
105+
return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
106+
}
107+
108+
/** What the customization read serves: one override per pair row. */
109+
function customizationRead() {
110+
const overrides = [...srv.pairRows.keys()].sort().map((key) => {
111+
const [type, name] = key.split('/') as [string, string];
112+
return { type, name, basedOn: '4.0.0' };
113+
});
114+
return {
115+
packageId: BASE,
116+
overrides,
117+
extensions: [],
118+
switchedOff: [],
119+
contributions: [],
120+
counts: { overrides: overrides.length, extensions: 0, switchedOff: 0, contributions: 0 },
121+
};
122+
}
123+
124+
beforeAll(() => {
125+
registerMetadataPreview('dashboard', LabelCanvas as never);
126+
registerMetadataPreview('page', LabelCanvas as never);
127+
});
128+
129+
beforeEach(() => {
130+
srv.pairRows.clear();
131+
srv.customizationReads = 0;
132+
srv.resets.length = 0;
133+
vi.stubGlobal(
134+
'fetch',
135+
vi.fn(async (input: unknown, init?: RequestInit) => {
136+
const url = new URL(String(input), 'http://localhost');
137+
const path = url.pathname;
138+
const method = init?.method ?? 'GET';
139+
if (path === '/api/v1/packages') {
140+
return json({
141+
success: true,
142+
data: [
143+
{ manifest: { id: BASE, name: 'HotCRM', namespace: 'hotcrm', version: '4.0.0' }, writable: false },
144+
{ manifest: { id: PAIR, name: 'HotCRM customizations', namespace: 'hotcrm_custom', version: '0.1.0' }, writable: true },
145+
],
146+
});
147+
}
148+
if (path === `/api/v1/packages/${BASE}/customizations`) {
149+
srv.customizationReads += 1;
150+
return json({ success: true, data: customizationRead() });
151+
}
152+
if (path === '/api/v1/meta/_drafts') return json({ drafts: [] });
153+
if (path === '/api/v1/meta/app') return json([{ name: 'crm_app', label: 'CRM' }]);
154+
if (path === '/api/v1/meta/app/crm_app/layers') return json({ code: APP, overlay: null, overlayScope: null, effective: APP });
155+
const layers = /^\/api\/v1\/meta\/([^/]+)\/([^/]+)\/layers$/.exec(path);
156+
if (layers) {
157+
const key = `${layers[1]}/${layers[2]}`;
158+
const code = BASE_ROWS[key] ?? null;
159+
const overlay = srv.pairRows.get(key) ?? null;
160+
return json({ code, overlay, overlayScope: overlay ? 'env' : null, effective: overlay ?? code });
161+
}
162+
const item = /^\/api\/v1\/meta\/([^/]+)\/([^/]+)$/.exec(path);
163+
if (item && method === 'DELETE') {
164+
const key = `${item[1]}/${item[2]}`;
165+
srv.resets.push(`${key}?${url.searchParams.toString()}`);
166+
if (url.searchParams.get('state') !== 'draft') srv.pairRows.delete(key);
167+
return json({ success: true });
168+
}
169+
if (item) return json({}, 404);
170+
const list = /^\/api\/v1\/meta\/([^/]+)$/.exec(path);
171+
if (list) {
172+
const type = list[1]!;
173+
return json(
174+
Object.entries(BASE_ROWS)
175+
.filter(([key]) => key.startsWith(`${type}/`))
176+
.map(([, row]) => ({ name: row.name, label: row.label, _packageId: BASE })),
177+
);
178+
}
179+
return json([]);
180+
}) as unknown as typeof fetch,
181+
);
182+
});
183+
184+
afterEach(() => {
185+
cleanup();
186+
vi.unstubAllGlobals();
187+
});
188+
189+
function renderWorkbench() {
190+
return render(
191+
<MemoryRouter initialEntries={[`/studio/${BASE}/interfaces`]}>
192+
<Routes>
193+
<Route path="/studio/:packageId/:tab" element={<StudioDesignSurface />} />
194+
</Routes>
195+
</MemoryRouter>,
196+
);
197+
}
198+
199+
const header = () => screen.getByTestId('if-item-header');
200+
const rail = () => screen.getByTestId('studio-rail-interfaces');
201+
const onlyText = (marked: number) => tFormat('engine.list.customizations.only', 'en', { marked });
202+
203+
/** The rail row named `name`, and its customization mark's kind (null: unmarked). */
204+
async function markOf(name: RegExp): Promise<string | null> {
205+
const row = await within(rail()).findByRole('button', { name }, { timeout: 8000 });
206+
return within(row).queryByTestId('customization-mark')?.getAttribute('data-kind') ?? null;
207+
}
208+
209+
async function restoreFromHeader(): Promise<void> {
210+
fireEvent.click(await within(header()).findByTestId('studio-override-chip', undefined, { timeout: 8000 }));
211+
const popover = await screen.findByTestId('studio-override-popover');
212+
await waitFor(() => expect(within(popover).getByTestId('studio-override-restore')).toBeEnabled(), { timeout: 8000 });
213+
fireEvent.click(within(popover).getByTestId('studio-override-restore'));
214+
fireEvent.click(within(await screen.findByTestId('studio-restore-original-dialog')).getByTestId('studio-restore-original-confirm'));
215+
}
216+
217+
describe('after Restore package original the rail reads the restored state, with no reload (objectstack#30238)', () => {
218+
it('a dashboard opened from the navigation: its mark and the "only customizations" count go', async () => {
219+
srv.pairRows.set('dashboard/pipeline', { ...BASE_ROWS['dashboard/pipeline'], label: 'Pipeline (ours)', overrides: DECLARED });
220+
renderWorkbench();
221+
await waitFor(() => expect(screen.getByTestId('canvas-label')).toHaveTextContent('Pipeline (ours)'), { timeout: 8000 });
222+
await waitFor(async () => expect(await markOf(/Pipeline/)).toBe('customized'), { timeout: 8000 });
223+
expect(await within(rail()).findByTestId('studio-rail-customizations-only')).toHaveTextContent(onlyText(1));
224+
const readsBefore = srv.customizationReads;
225+
226+
await restoreFromHeader();
227+
await waitFor(() => expect(screen.getByTestId('canvas-label')).toHaveTextContent(/^Pipeline$/), { timeout: 8000 });
228+
expect(srv.resets.map((r) => r.split('?')[0])).toEqual(['dashboard/pipeline', 'dashboard/pipeline']);
229+
await waitFor(() => expect(srv.customizationReads).toBeGreaterThan(readsBefore), { timeout: 8000 });
230+
await waitFor(async () => expect(await markOf(/Pipeline/)).toBeNull(), { timeout: 8000 });
231+
await waitFor(() => expect(within(rail()).queryByTestId('studio-rail-customizations-only')).toBeNull(), { timeout: 8000 });
232+
});
233+
234+
it('a page listed under its type: its mark and the count go; CONTROL: the dashboard still customized keeps both', async () => {
235+
srv.pairRows.set('dashboard/forecast', { ...BASE_ROWS['dashboard/forecast'], label: 'Forecast (ours)', overrides: DECLARED });
236+
srv.pairRows.set('page/crm_welcome', { ...BASE_ROWS['page/crm_welcome'], label: 'Welcome (ours)', overrides: DECLARED });
237+
renderWorkbench();
238+
await waitFor(() => expect(screen.getByTestId('canvas-label')).toHaveTextContent(/^Pipeline$/), { timeout: 8000 });
239+
await waitFor(async () => expect(await markOf(/Welcome/)).toBe('customized'), { timeout: 8000 });
240+
expect(await markOf(/Forecast/)).toBe('customized');
241+
expect(await within(rail()).findByTestId('studio-rail-customizations-only')).toHaveTextContent(onlyText(2));
242+
243+
fireEvent.click(await within(rail()).findByRole('button', { name: /Welcome/ }));
244+
await waitFor(() => expect(screen.getByTestId('canvas-label')).toHaveTextContent('Welcome (ours)'), { timeout: 8000 });
245+
const readsBefore = srv.customizationReads;
246+
await restoreFromHeader();
247+
await waitFor(() => expect(screen.getByTestId('canvas-label')).toHaveTextContent(/^Welcome$/), { timeout: 8000 });
248+
await waitFor(() => expect(srv.customizationReads).toBeGreaterThan(readsBefore), { timeout: 8000 });
249+
await waitFor(async () => expect(await markOf(/Welcome/)).toBeNull(), { timeout: 8000 });
250+
await waitFor(() => expect(within(rail()).getByTestId('studio-rail-customizations-only')).toHaveTextContent(onlyText(1)), {
251+
timeout: 8000,
252+
});
253+
expect(await markOf(/Forecast/)).toBe('customized');
254+
});
255+
});

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -854,9 +854,19 @@ function StudioDesignSurfaceBody({ aiSlot, area }: StudioDesignSurfaceProps): Re
854854
const [publishNonce, setPublishNonce] = React.useState(0); // ↑ → pillars re-read the published baseline
855855
const [draftNonce, setDraftNonce] = React.useState(0); // ↑ → a draft save landed (`onDraftSaved`)
856856
// objectstack#28572 (ADR-0146 D3) — the base's served customization read
857-
// (N-15), which every pillar rail marks its rows from; read again after a
858-
// publish, which is what moves the served overrides.
859-
const baseCustomizations = usePackageCustomizations(baseRead && packageId !== null ? packageId : null, publishNonce);
857+
// (N-15), which every pillar rail marks its rows from; read again whenever
858+
// the served overrides may have moved. objectstack#30238 — that is the
859+
// metadata-refresh pulse, not `publishNonce`: a publish moves the nonce AND
860+
// announces itself on the bus, but "Restore package original" (every
861+
// pillar's, and the bundle item editor's) deletes the pair's row and only
862+
// announces, so a read keyed on the nonce left the rail's marks and the
863+
// "only customizations" count stale until a reload. Keyed on the pulse
864+
// alone, a publish reads once, not twice; a copilot publish is read too.
865+
const customizationsRefresh = useMetadataRefreshNonce();
866+
const baseCustomizations = usePackageCustomizations(
867+
baseRead && packageId !== null ? packageId : null,
868+
customizationsRefresh,
869+
);
860870
const customization = React.useMemo<StudioBaseCustomization | null>(
861871
() =>
862872
baseRead && packageId !== null

0 commit comments

Comments
 (0)