From 23897e3b8ed0ac224e9cdc988a1d8a14ebd39afe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 19:15:16 +0000 Subject: [PATCH 1/4] wip(console): read an installed app's remaining sample rows on any visit (#30616) --- .../marketplace/MarketplacePackagePage.tsx | 63 +++++++-- ...ackagePage.sampleDataNotice-30323.test.tsx | 129 +++++++++++++++++- .../src/console/marketplace/marketplaceApi.ts | 34 +++++ .../console/marketplace/sampleDataNotice.ts | 47 ++++++- 4 files changed, 253 insertions(+), 20 deletions(-) diff --git a/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx b/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx index d30a00ce15b..3ab826d5e07 100644 --- a/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx +++ b/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx @@ -81,6 +81,7 @@ import { purgeSampleData, reseedLocalSampleData, purgeLocalSampleData, + countLocalSampleData, type MarketplaceDetailResponse, type CloudEnvironment, type LocalInstallEntry, @@ -92,7 +93,15 @@ import { import { useDependencyMissingText } from './dependencyMissing.js'; import { useProtocolCheckSkippedText } from './protocolCheckSkipped.js'; import { ProtocolCheckSkippedNote } from './ProtocolCheckSkippedNote.js'; -import { forgetSampleData, rememberSampleData, sampleDataNotice, sampleRecordsLoaded, takeClearConfirmRequest } from './sampleDataNotice.js'; +import { + forgetSampleData, + noteServedSampleCount, + rememberSampleData, + sampleDataGeneration, + sampleDataNotice, + sampleRecordsLoaded, + takeClearConfirmRequest, +} from './sampleDataNotice.js'; import { SampleDataLeaveBanner } from './SampleDataLeaveBanner.js'; import { getCloudBase, getRuntimeConfig, isMarketplaceEnabled } from '../../runtime-config.js'; import { emitMetadataRefresh } from '../../assistant/assistantBus.js'; @@ -289,9 +298,13 @@ export function MarketplacePackagePage() { sampleData?: { count: number; reinstall: boolean }; } | null>(null); // objectstack#30323 — the console's own confirm for a sample-data purge, in - // place of the browser's: which install it purges, and how many sample - // records this session knows that install loaded (`null`: none known). - const [purgeConfirm, setPurgeConfirm] = useState<{ target: 'local' | 'cloud'; count: number | null } | null>(null); + // place of the browser's: which install it purges. The count it draws for a + // local purge is read when it is drawn (`localSampleCount` below), so a + // count the runtime answers while it is open still reaches it. + const [purgeConfirm, setPurgeConfirm] = useState<{ target: 'local' | 'cloud' } | null>(null); + // objectstack#30616 — bumped when the runtime's count of this page's local + // install's sample rows lands in the session record, so the page draws it. + const [, setSampleCountReads] = useState(0); const hasNotificationProvider = useHasNotificationProvider(); // ADR-0131 D6 — the install modes the package's latest manifest declares, // read from the manifest install-local judges, and the operator's pick among @@ -331,7 +344,7 @@ export function MarketplacePackagePage() { // page it opens, so the confirm opens once the listing says that // package's sample data is still there. const requested = items.find((i) => i.withSampleData && !i.notLoaded && takeClearConfirmRequest(i.manifestId)); - if (requested) setPurgeConfirm({ target: 'local', count: sampleDataNotice(requested.manifestId)?.count ?? null }); + if (requested) setPurgeConfirm({ target: 'local' }); })(); return () => { cancelled = true; }; }, [packageId, localResult, isAdmin]); @@ -898,12 +911,36 @@ export function MarketplacePackagePage() { ? localInstalls.find((i) => !!data && i.manifestId === data.package.manifest_id) : localInstalls.find((i) => i.packageId === packageId)) ?? null; + // objectstack#30616 — the runtime's count of the sample rows a purge of this + // install would delete now, read on every visit and again each time the + // listing is read (after an install, a reseed or a clear, which hand the + // page a new entry). It goes into the session record the confirm and the + // banner read: above zero it is the count they draw, zero drops the record + // and its banner. An answer of this session's own that lands while the read + // is in flight is fresher and stays (`noteServedSampleCount`). A failed read + // changes nothing: the record keeps what this session knows, if anything. + useEffect(() => { + if (!localInstall || localInstall.notLoaded) return; + const manifestId = localInstall.manifestId; + const generation = sampleDataGeneration(manifestId); + let cancelled = false; + (async () => { + const served = await countLocalSampleData(manifestId); + if (cancelled || !served) return; + const name = data?.package ? localizePackage(data.package as any, language).displayName : manifestId; + noteServedSampleCount({ manifestId, name, count: served.count, path: `${basePath}/system/marketplace/${packageId}` }, generation); + setSampleCountReads((n) => n + 1); + })(); + return () => { cancelled = true; }; + }, [localInstall, data, language, basePath, packageId]); + // objectstack#30323 — the console's confirm for a local purge, with the - // count this session knows the install left in place, when it knows one. + // count this session knows the install's purge would delete, when it knows one. const openLocalPurgeConfirm = () => { if (!localInstall) return; - setPurgeConfirm({ target: 'local', count: sampleDataNotice(localInstall.manifestId)?.count ?? null }); + setPurgeConfirm({ target: 'local' }); }; + const localSampleCount = localInstall ? sampleDataNotice(localInstall.manifestId)?.count ?? null : null; // The local-install menu and the two status banners, drawn by both the // catalog view and the marketplace-off local view below (objectui#11627). @@ -996,16 +1033,18 @@ export function MarketplacePackagePage() { ); // objectstack#30323 — the purge confirm, in the console's own dialog. The - // count line is drawn only when this session knows what the install loaded: - // the runtime serves no count of a package's sample rows after that answer. + // count line is drawn for a local purge whose count this session knows: the + // runtime's (objectstack#30616) or, when that could not be read, the one the + // install answered. A cloud purge draws none. + const purgeConfirmCount = purgeConfirm?.target === 'local' ? localSampleCount : null; const purgeConfirmDialog = ( { if (!open) setPurgeConfirm(null); }}> {t('marketplace.detail.purgeSampleData')} - {purgeConfirm?.count != null - ? `${t('marketplace.detail.purgeConfirmAtMost', { count: purgeConfirm.count })}\n${t('marketplace.detail.purgeConfirm')}` + {purgeConfirmCount != null + ? `${t('marketplace.detail.purgeConfirmAtMost', { count: purgeConfirmCount })}\n${t('marketplace.detail.purgeConfirm')}` : t('marketplace.detail.purgeConfirm')} @@ -1509,7 +1548,7 @@ export function MarketplacePackagePage() { : t('marketplace.detail.addSampleData')} setPurgeConfirm({ target: 'cloud', count: null })} + onSelect={() => setPurgeConfirm({ target: 'cloud' })} disabled={sampleDataBusy !== null || cloudSampleDataRefused || !cloudInstall.withSampleData} className="text-destructive focus:text-destructive" > diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataNotice-30323.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataNotice-30323.test.tsx index dbb67571e6d..70559457cd7 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataNotice-30323.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataNotice-30323.test.tsx @@ -5,6 +5,9 @@ * records the install loaded and offers the clear right there; the clear is * confirmed in the console's own dialog, with that count; and a banner * follows the installer to the app once they leave the page (objectstack#30323). + * After a reload, the runtime's own count of the sample rows a purge would + * delete (`GET …/sample-data`) fills the same count and banner on any visit + * (objectstack#30616). * * Sample data stays ON by default (the maintainer's ruling on the card): every * case installs WITH it, and what is pinned is how it is announced and cleared. @@ -55,10 +58,17 @@ vi.mock('../ConnectorCredentialsStep', () => ({ import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config'; import { MarketplacePackagePage } from '../MarketplacePackagePage'; -import { resetSampleDataSessionForTesting } from '../sampleDataNotice'; +import { + noteServedSampleCount, + rememberSampleData, + resetSampleDataSessionForTesting, + sampleDataGeneration, + sampleDataNotice, +} from '../sampleDataNotice'; const LEDGER = '/api/v1/marketplace/install-local'; const PURGE = `${LEDGER}/com.acme.crm/purge-sample-data`; +const COUNT = `${LEDGER}/com.acme.crm/sample-data`; const CATALOG_PACKAGE = { package: { id: 'pkg_crm', manifest_id: 'com.acme.crm', display_name: 'Acme CRM', latest_version: { id: 'ver_1', version: '6.0.0' } }, versions: [{ id: 'ver_1', version: '6.0.0' }], @@ -78,8 +88,13 @@ const answer = (status: number, body: unknown) => ({ let calls: Array<{ method: string; path: string }> = []; -/** `installed`: the ledger before the install; the install puts the entry (with sample data) in it. */ -async function boot(installData: Record, opts: { installed?: Record } = {}) { +/** + * `installed`: the ledger before the install; the install puts the entry (with sample data) in it. + * `served`: what `GET …/sample-data` answers, per call (the n-th read gets the n-th entry, the last + * repeats); a function is awaited, so a read can be held. Absent: the route answers 404. + */ +type Served = { count: number; unidentified?: number } | number | (() => Promise<{ count: number }>); +async function boot(installData: Record, opts: { installed?: Record; served?: Served[] } = {}) { resetRuntimeConfigForTesting(); calls = []; let ledger: Array> = opts.installed ? [opts.installed] : []; @@ -99,6 +114,14 @@ async function boot(installData: Record, opts: { installed?: Re ledger = [{ ...ENTRY, withSampleData: true }]; return answer(200, { success: true, data: installData }); } + if (path === COUNT && method === 'GET' && opts.served) { + const reads = calls.filter((c) => c.path === COUNT).length; + const next = opts.served[Math.min(reads, opts.served.length) - 1]; + const body = typeof next === 'function' ? await next() : next; + return typeof body === 'number' + ? answer(body, { success: false, error: { code: 'FORBIDDEN', message: 'refused' } }) + : answer(200, { success: true, data: { manifestId: 'com.acme.crm', unidentified: 0, ...body } }); + } if (path === PURGE && method === 'POST') { ledger = ledger.map((e) => ({ ...e, withSampleData: false })); return answer(200, { success: true, data: { manifestId: 'com.acme.crm', deleted: 369, skipped: 0, errors: 0, withSampleData: false } }); @@ -282,3 +305,103 @@ describe('a banner follows the installer to the app\'s pages (objectstack#30323) expect(document.querySelector('[data-notification-surface="banner"]')).toBeNull(); }); }); + +describe('after a reload, the runtime\'s count fills the confirm and the banner (objectstack#30616)', () => { + const countReads = () => calls.filter((c) => c.path === COUNT); + + async function openMenuPurge() { + fireEvent.pointerDown(await screen.findByRole('button', { name: '«marketplace.detail.moreOptions»' }), { button: 0, ctrlKey: false }); + fireEvent.click(await screen.findByRole('menuitem', { name: '«marketplace.detail.purgeSampleData»' })); + return screen.findByTestId('purge-sample-data-dialog'); + } + + it('a visit with no install this session reads the count, and the confirm says it', async () => { + await boot({ ...MANAGED }, { installed: { ...ENTRY, withSampleData: true }, served: [{ count: 42 }] }); + render(); + + await waitFor(() => expect(countReads()).toEqual([{ method: 'GET', path: COUNT }])); + const dialog = await openMenuPurge(); + await waitFor(() => expect(dialog).toHaveTextContent('«marketplace.detail.purgeConfirmAtMost|count=42»')); + expect(dialog).toHaveTextContent('«marketplace.detail.purgeConfirm»'); + }); + + it('the count raises the banner on leaving, as an install of this session does', async () => { + await boot({ ...MANAGED }, { installed: { ...ENTRY, withSampleData: true }, served: [{ count: 42 }] }); + const view = render(); + await waitFor(() => expect(sampleDataNotice('com.acme.crm')?.count).toBe(42)); + + view.rerender(); + expect(await screen.findByText('«marketplace.detail.sampleDataBannerTitle|name=Acme CRM»')).toBeInTheDocument(); + }); + + it('`count: 0` drops the record this session held: no banner on leaving, no count in the confirm', async () => { + rememberSampleData({ manifestId: 'com.acme.crm', name: 'Acme CRM', count: 369, path: '/apps/setup/system/marketplace/pkg_crm' }); + await boot({ ...MANAGED }, { installed: { ...ENTRY, withSampleData: true }, served: [{ count: 0 }] }); + const view = render(); + await waitFor(() => expect(countReads()).toHaveLength(1)); + await waitFor(() => expect(sampleDataNotice('com.acme.crm')).toBeUndefined()); + + const dialog = await openMenuPurge(); + expect(dialog).toHaveTextContent('«marketplace.detail.purgeConfirm»'); + expect(dialog).not.toHaveTextContent('purgeConfirmAtMost'); + fireEvent.click(within(dialog).getByRole('button', { name: '«common.cancel»' })); + + view.rerender(); + await act(async () => {}); + expect(screen.queryByText('«marketplace.detail.sampleDataBannerTitle|name=Acme CRM»')).not.toBeInTheDocument(); + }); + + it('an install is read again: the runtime\'s count, read after the install answered, is the one drawn', async () => { + await boot({ ...MANAGED, seeded: SEEDED_369 }, { served: [{ count: 360 }] }); + await install(); + await screen.findByTestId('install-sample-data'); + await waitFor(() => expect(countReads()).toHaveLength(1)); + + fireEvent.click(await screen.findByTestId('install-sample-data-clear')); + const dialog = await screen.findByTestId('purge-sample-data-dialog'); + await waitFor(() => expect(dialog).toHaveTextContent('«marketplace.detail.purgeConfirmAtMost|count=360»')); + // The install note keeps what the install answered. + expect(screen.getByTestId('install-sample-data')).toHaveTextContent('«marketplace.detail.sampleDataLoaded|count=369»'); + }); + + it('CONTROL: a refused or failed read keeps the count this session\'s install answered', async () => { + await boot({ ...MANAGED, seeded: SEEDED_369 }, { served: [403] }); + await install(); + await screen.findByTestId('install-sample-data'); + await waitFor(() => expect(countReads()).toHaveLength(1)); + + fireEvent.click(await screen.findByTestId('install-sample-data-clear')); + const dialog = await screen.findByTestId('purge-sample-data-dialog'); + expect(dialog).toHaveTextContent('«marketplace.detail.purgeConfirmAtMost|count=369»'); + }); + + it('CONTROL: a package this runtime did not load is not counted', async () => { + await boot({ ...MANAGED }, { + installed: { ...ENTRY, notLoaded: { code: 'OS_PROTOCOL_INCOMPATIBLE', requiredRange: '^99.0.0' } }, + served: [{ count: 42 }], + }); + render(); + await screen.findByRole('button', { name: '«marketplace.detail.moreOptions»' }); + await act(async () => {}); + expect(countReads()).toEqual([]); + }); +}); + +describe('a count read before an answer of this session is older than it (objectstack#30616)', () => { + const notice = { manifestId: 'com.acme.crm', name: 'Acme CRM', path: '/p' }; + + it('a read that began before an install answered does not replace that install\'s count', () => { + const generation = sampleDataGeneration('com.acme.crm'); + rememberSampleData({ ...notice, count: 369 }); + noteServedSampleCount({ ...notice, count: 0 }, generation); + expect(sampleDataNotice('com.acme.crm')?.count).toBe(369); + }); + + it('a read that began after it does', () => { + rememberSampleData({ ...notice, count: 369 }); + noteServedSampleCount({ ...notice, count: 12 }, sampleDataGeneration('com.acme.crm')); + expect(sampleDataNotice('com.acme.crm')?.count).toBe(12); + noteServedSampleCount({ ...notice, count: 0 }, sampleDataGeneration('com.acme.crm')); + expect(sampleDataNotice('com.acme.crm')).toBeUndefined(); + }); +}); diff --git a/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts b/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts index 5cfc7f78c12..ea47fa7f28e 100644 --- a/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts +++ b/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts @@ -1085,3 +1085,37 @@ export function reseedLocalSampleData(manifestId: string): Promise { return postLocalSampleAction(manifestId, 'purge'); } + +/** + * `GET /api/v1/marketplace/install-local/:manifestId/sample-data` + * (objectstack#30616): how many of a local install's sample rows a purge + * would delete right now, counted with the purge's own matcher and keep-rules, + * so a page reached after a reload can still say it. `unidentified` are seed + * records the purge would report and leave in place. + */ +export interface LocalSampleDataCount { + manifestId: string; + count: number; + unidentified: number; +} + +/** {@link LocalSampleDataCount}, or `null` when the count could not be read (refused, failed, or no count in the answer). */ +export async function countLocalSampleData(manifestId: string): Promise { + try { + const res = await fetch(`${API_BASE}/install-local/${encodeURIComponent(manifestId)}/sample-data`, { + credentials: 'include', + headers: { 'Accept': 'application/json' }, + }); + if (!res.ok) return null; + const payload: any = await res.json().catch(() => ({})); + const data: any = payload?.data ?? payload; + if (typeof data?.count !== 'number' || !Number.isInteger(data.count) || data.count < 0) return null; + return { + manifestId, + count: data.count, + unidentified: typeof data.unidentified === 'number' ? data.unidentified : 0, + }; + } catch { + return null; + } +} diff --git a/ui/packages/app-shell/src/console/marketplace/sampleDataNotice.ts b/ui/packages/app-shell/src/console/marketplace/sampleDataNotice.ts index 331d4cc506f..e677efc63d6 100644 --- a/ui/packages/app-shell/src/console/marketplace/sampleDataNotice.ts +++ b/ui/packages/app-shell/src/console/marketplace/sampleDataNotice.ts @@ -15,10 +15,12 @@ * back, and the banner that follows the installer to the app's own pages * (`SampleDataLeaveBanner`) is raised from it and withdrawn through it. * - * The record is in memory only, and only for installs made in this session: - * the runtime serves no count of a package's sample rows after the install - * answer (its listing answers present or not), so a reload drops the count - * rather than guessing one. + * The record is in memory only. A reload drops it, and the package page fills + * it again from the runtime's own count of the sample rows a purge would + * delete now (`GET …/install-local/:manifestId/sample-data`, objectstack#30616) + * through {@link noteServedSampleCount}: a count above zero records it, zero + * drops the record and its banner. An answer this session learned after that + * read began (an install, a reseed or a clear) is fresher and is kept. */ import type { LocalInstallSeeded } from './marketplaceApi.js'; @@ -43,7 +45,7 @@ export interface SampleDataNotice { manifestId: string; /** The package's display name, as the install note named it. */ name: string; - /** Sample records the install left in place ({@link sampleRecordsLoaded}). */ + /** Sample records the install left in place ({@link sampleRecordsLoaded}), or the runtime's count of them ({@link noteServedSampleCount}). */ count: number; /** The package page, where the clear confirm is drawn. */ path: string; @@ -59,12 +61,45 @@ interface SessionEntry { const session = new Map(); +/** + * Per package, how many times this session learned its sample data from an + * answer of its own (install, reseed, clear): a served count read before the + * latest of them is older than it, and is not recorded. + */ +const learned = new Map(); +const learn = (manifestId: string) => learned.set(manifestId, (learned.get(manifestId) ?? 0) + 1); + /** Records what an install loaded, replacing an earlier record for the package. */ export function rememberSampleData(notice: SampleDataNotice): void { + learn(notice.manifestId); session.get(notice.manifestId)?.withdrawBanner?.(); session.set(notice.manifestId, { notice }); } +/** What {@link noteServedSampleCount} compares against: read it when the count's request starts. */ +export function sampleDataGeneration(manifestId: string): number { + return learned.get(manifestId) ?? 0; +} + +/** + * Records the runtime's count of a package's sample rows, read on a visit to + * its page. A count above zero updates the record in place (a standing banner + * and a pending confirm request stay with it) or starts one; zero drops the + * record and withdraws its banner. Ignored when this session learned the + * package's sample data from an answer after `generation` was read. + */ +export function noteServedSampleCount(notice: SampleDataNotice, generation: number): void { + if (sampleDataGeneration(notice.manifestId) !== generation) return; + const entry = session.get(notice.manifestId); + if (notice.count <= 0) { + entry?.withdrawBanner?.(); + session.delete(notice.manifestId); + return; + } + if (entry) entry.notice = notice; + else session.set(notice.manifestId, { notice }); +} + /** The record for a package, or `undefined` when this session installed it with no sample data. */ export function sampleDataNotice(manifestId: string): SampleDataNotice | undefined { return session.get(manifestId)?.notice; @@ -72,6 +107,7 @@ export function sampleDataNotice(manifestId: string): SampleDataNotice | undefin /** Drops the record and its banner: the sample data was cleared. */ export function forgetSampleData(manifestId: string): void { + learn(manifestId); session.get(manifestId)?.withdrawBanner?.(); session.delete(manifestId); } @@ -106,4 +142,5 @@ export function takeClearConfirmRequest(manifestId: string): boolean { /** Test-only: an empty session. */ export function resetSampleDataSessionForTesting(): void { session.clear(); + learned.clear(); } From ac4fb17b4876fedd1501f89b5c2b2c5a01c4038a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 19:22:34 +0000 Subject: [PATCH 2/4] fix(i18n): the sample-data purge confirm's count line holds for the runtime's count too (#30616) `purgeConfirmAtMost` said the count was "the number this install loaded", which is false once the console draws the runtime's count of the rows a purge would delete now. All ten packs drop that clause and keep the upper bound, which holds for both sources. No key added; en shrinks. Claude-Session: https://claude.ai/code/session_01L6bhgTRFYJtEzJqfKEhD7C Co-authored-by: Claude --- ui/packages/i18n/src/locales/ar.ts | 14 +++++++------- ui/packages/i18n/src/locales/de.ts | 6 +++--- ui/packages/i18n/src/locales/en.ts | 6 +++--- ui/packages/i18n/src/locales/es.ts | 8 ++++---- ui/packages/i18n/src/locales/fr.ts | 8 ++++---- ui/packages/i18n/src/locales/ja.ts | 6 +++--- ui/packages/i18n/src/locales/ko.ts | 6 +++--- ui/packages/i18n/src/locales/pt.ts | 8 ++++---- ui/packages/i18n/src/locales/ru.ts | 10 +++++----- ui/packages/i18n/src/locales/zh.ts | 6 +++--- 10 files changed, 39 insertions(+), 39 deletions(-) diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index c2add4c92bc..815968425bb 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -4524,13 +4524,13 @@ const ar = { sampleDataLoaded_many: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', sampleDataLoaded_other: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', sampleDataReloaded: 'يؤدي التثبيت مرة أخرى إلى تحميل البيانات التجريبية من جديد، بما فيها ما حذفته.', - purgeConfirmAtMost: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر: العدد الذي حمّله هذا التثبيت.', - purgeConfirmAtMost_zero: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر: العدد الذي حمّله هذا التثبيت.', - purgeConfirmAtMost_one: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر: العدد الذي حمّله هذا التثبيت.', - purgeConfirmAtMost_two: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر: العدد الذي حمّله هذا التثبيت.', - purgeConfirmAtMost_few: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر: العدد الذي حمّله هذا التثبيت.', - purgeConfirmAtMost_many: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر: العدد الذي حمّله هذا التثبيت.', - purgeConfirmAtMost_other: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر: العدد الذي حمّله هذا التثبيت.', + purgeConfirmAtMost: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_zero: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_one: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_two: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_few: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_many: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_other: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', sampleDataBannerTitle: 'يحتوي "{{name}}" على بيانات تجريبية.', sampleDataBannerMessage: 'إنها موجودة لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', reseedQueued: 'ستتم إعادة تحميل البيانات التجريبية عند الوصول التالي إلى البيئة.', diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index b9c43ae38d6..e2d74f3fabd 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -4179,9 +4179,9 @@ const de = { sampleDataLoaded_one: 'Diese Installation hat {{count}} Beispieldatensatz geladen, damit Sie die App in Aktion sehen. Löschen Sie ihn, sobald Sie die App wirklich nutzen.', sampleDataLoaded_other: 'Diese Installation hat {{count}} Beispieldatensätze geladen, damit Sie die App in Aktion sehen. Löschen Sie sie, sobald Sie die App wirklich nutzen.', sampleDataReloaded: 'Eine erneute Installation lädt die Beispieldaten wieder, auch die bereits gelöschten.', - purgeConfirmAtMost: 'Höchstens {{count}} Beispieldatensatz/-sätze werden gelöscht: so viele, wie diese Installation geladen hat.', - purgeConfirmAtMost_one: 'Höchstens {{count}} Beispieldatensatz wird gelöscht: so viele, wie diese Installation geladen hat.', - purgeConfirmAtMost_other: 'Höchstens {{count}} Beispieldatensätze werden gelöscht: so viele, wie diese Installation geladen hat.', + purgeConfirmAtMost: 'Höchstens {{count}} Beispieldatensatz/-sätze werden gelöscht.', + purgeConfirmAtMost_one: 'Höchstens {{count}} Beispieldatensatz wird gelöscht.', + purgeConfirmAtMost_other: 'Höchstens {{count}} Beispieldatensätze werden gelöscht.', sampleDataBannerTitle: '„{{name}}“ enthält Beispieldaten.', sampleDataBannerMessage: 'Sie zeigen, wie die App arbeitet. Löschen Sie sie, sobald Sie die App wirklich nutzen.', reseedQueued: 'Beispieldaten werden beim nächsten Zugriff auf die Umgebung neu geladen.', diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index 2b432e11bff..d595413e5bc 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -4981,9 +4981,9 @@ const en = { sampleDataLoaded_one: 'This install loaded {{count}} sample record so you can see the app working. Clear it when you start using the app for real.', sampleDataLoaded_other: 'This install loaded {{count}} sample records so you can see the app working. Clear them when you start using it for real.', sampleDataReloaded: 'Installing again loads the sample data again, including any you had cleared.', - purgeConfirmAtMost: 'At most {{count}} sample record(s) will be deleted: the number this install loaded.', - purgeConfirmAtMost_one: 'At most {{count}} sample record will be deleted: the number this install loaded.', - purgeConfirmAtMost_other: 'At most {{count}} sample records will be deleted: the number this install loaded.', + purgeConfirmAtMost: 'Up to {{count}} sample record(s) will be deleted.', + purgeConfirmAtMost_one: 'Up to {{count}} sample record will be deleted.', + purgeConfirmAtMost_other: 'Up to {{count}} sample records will be deleted.', sampleDataBannerTitle: '"{{name}}" has sample data.', sampleDataBannerMessage: 'It is there so you can see the app working. Clear it when you start using the app for real.', reseedQueued: 'Sample data will be re-seeded on next environment access.', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index 4e70bd38085..fab7f86e5cb 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -4266,10 +4266,10 @@ const es = { sampleDataLoaded_many: 'Esta instalación cargó {{count}} de registros de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', sampleDataLoaded_other: 'Esta instalación cargó {{count}} registros de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', sampleDataReloaded: 'Instalar de nuevo vuelve a cargar los datos de ejemplo, incluidos los que haya eliminado.', - purgeConfirmAtMost: 'Se eliminarán como máximo {{count}} registro(s) de ejemplo: los que cargó esta instalación.', - purgeConfirmAtMost_one: 'Se eliminará como máximo {{count}} registro de ejemplo: el que cargó esta instalación.', - purgeConfirmAtMost_many: 'Se eliminarán como máximo {{count}} de registros de ejemplo: los que cargó esta instalación.', - purgeConfirmAtMost_other: 'Se eliminarán como máximo {{count}} registros de ejemplo: los que cargó esta instalación.', + purgeConfirmAtMost: 'Se eliminarán como máximo {{count}} registro(s) de ejemplo.', + purgeConfirmAtMost_one: 'Se eliminará como máximo {{count}} registro de ejemplo.', + purgeConfirmAtMost_many: 'Se eliminarán como máximo {{count}} de registros de ejemplo.', + purgeConfirmAtMost_other: 'Se eliminarán como máximo {{count}} registros de ejemplo.', sampleDataBannerTitle: '"{{name}}" tiene datos de ejemplo.', sampleDataBannerMessage: 'Están ahí para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', reseedQueued: 'Los datos de ejemplo se volverán a cargar en el próximo acceso al entorno.', diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index 34e09486782..d31e0817959 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -4262,10 +4262,10 @@ const fr = { sampleDataLoaded_many: 'Cette installation a chargé {{count}} d\'enregistrements d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', sampleDataLoaded_other: 'Cette installation a chargé {{count}} enregistrements d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', sampleDataReloaded: 'Réinstaller recharge les données d\'exemple, y compris celles que vous avez supprimées.', - purgeConfirmAtMost: 'Au plus {{count}} enregistrement(s) d\'exemple seront supprimés : le nombre chargé par cette installation.', - purgeConfirmAtMost_one: 'Au plus {{count}} enregistrement d\'exemple sera supprimé : le nombre chargé par cette installation.', - purgeConfirmAtMost_many: 'Au plus {{count}} d\'enregistrements d\'exemple seront supprimés : le nombre chargé par cette installation.', - purgeConfirmAtMost_other: 'Au plus {{count}} enregistrements d\'exemple seront supprimés : le nombre chargé par cette installation.', + purgeConfirmAtMost: 'Au plus {{count}} enregistrement(s) d\'exemple seront supprimés.', + purgeConfirmAtMost_one: 'Au plus {{count}} enregistrement d\'exemple sera supprimé.', + purgeConfirmAtMost_many: 'Au plus {{count}} d\'enregistrements d\'exemple seront supprimés.', + purgeConfirmAtMost_other: 'Au plus {{count}} enregistrements d\'exemple seront supprimés.', sampleDataBannerTitle: '« {{name}} » contient des données d\'exemple.', sampleDataBannerMessage: 'Elles sont là pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', reseedQueued: 'Les données d\'exemple seront rechargées au prochain accès à l\'environnement.', diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index 24176ed28ad..8e785a069b5 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -4181,9 +4181,9 @@ const ja = { sampleDataLoaded_one: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', sampleDataLoaded_other: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', sampleDataReloaded: 'もう一度インストールすると、削除したものも含めてサンプルデータが再び読み込まれます。', - purgeConfirmAtMost: '削除されるサンプルレコードは最大 {{count}} 件です(このインストールで読み込んだ件数)。', - purgeConfirmAtMost_one: '削除されるサンプルレコードは最大 {{count}} 件です(このインストールで読み込んだ件数)。', - purgeConfirmAtMost_other: '削除されるサンプルレコードは最大 {{count}} 件です(このインストールで読み込んだ件数)。', + purgeConfirmAtMost: '削除されるサンプルレコードは最大 {{count}} 件です。', + purgeConfirmAtMost_one: '削除されるサンプルレコードは最大 {{count}} 件です。', + purgeConfirmAtMost_other: '削除されるサンプルレコードは最大 {{count}} 件です。', sampleDataBannerTitle: '「{{name}}」にはサンプルデータがあります。', sampleDataBannerMessage: 'アプリの動作を確認するためのデータです。本番で使い始める前に削除できます。', reseedQueued: 'サンプルデータは次回の環境アクセス時に再投入されます。', diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 21267948d08..fa3e7b312c2 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -4178,9 +4178,9 @@ const ko = { sampleDataLoaded_one: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', sampleDataLoaded_other: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', sampleDataReloaded: '다시 설치하면 삭제한 것을 포함해 샘플 데이터를 다시 불러옵니다.', - purgeConfirmAtMost: '최대 {{count}}개의 샘플 레코드가 삭제됩니다(이번 설치로 불러온 수).', - purgeConfirmAtMost_one: '최대 {{count}}개의 샘플 레코드가 삭제됩니다(이번 설치로 불러온 수).', - purgeConfirmAtMost_other: '최대 {{count}}개의 샘플 레코드가 삭제됩니다(이번 설치로 불러온 수).', + purgeConfirmAtMost: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', + purgeConfirmAtMost_one: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', + purgeConfirmAtMost_other: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', sampleDataBannerTitle: '"{{name}}"에 샘플 데이터가 있습니다.', sampleDataBannerMessage: '앱이 동작하는 모습을 확인하기 위한 데이터입니다. 실제로 사용하기 전에 삭제할 수 있습니다.', reseedQueued: '샘플 데이터는 다음 환경 접속 시 다시 적재됩니다.', diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index e403efd6df2..9e6914ef5e0 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -4261,10 +4261,10 @@ const pt = { sampleDataLoaded_many: 'Esta instalação carregou {{count}} de registros de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', sampleDataLoaded_other: 'Esta instalação carregou {{count}} registros de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', sampleDataReloaded: 'Instalar de novo carrega os dados de exemplo outra vez, inclusive os que você removeu.', - purgeConfirmAtMost: 'No máximo {{count}} registro(s) de exemplo serão excluídos: o número que esta instalação carregou.', - purgeConfirmAtMost_one: 'No máximo {{count}} registro de exemplo será excluído: o número que esta instalação carregou.', - purgeConfirmAtMost_many: 'No máximo {{count}} de registros de exemplo serão excluídos: o número que esta instalação carregou.', - purgeConfirmAtMost_other: 'No máximo {{count}} registros de exemplo serão excluídos: o número que esta instalação carregou.', + purgeConfirmAtMost: 'No máximo {{count}} registro(s) de exemplo serão excluídos.', + purgeConfirmAtMost_one: 'No máximo {{count}} registro de exemplo será excluído.', + purgeConfirmAtMost_many: 'No máximo {{count}} de registros de exemplo serão excluídos.', + purgeConfirmAtMost_other: 'No máximo {{count}} registros de exemplo serão excluídos.', sampleDataBannerTitle: '"{{name}}" tem dados de exemplo.', sampleDataBannerMessage: 'Eles estão lá para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', reseedQueued: 'Os dados de exemplo serão recarregados no próximo acesso ao ambiente.', diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index 00e049aa3b3..b992c23aab9 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -4362,11 +4362,11 @@ const ru = { sampleDataLoaded_many: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', sampleDataLoaded_other: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', sampleDataReloaded: 'Повторная установка снова загружает демоданные, включая удалённые.', - purgeConfirmAtMost: 'Будет удалено не больше демонстрационных записей, чем загрузила эта установка: {{count}}.', - purgeConfirmAtMost_one: 'Будет удалено не больше демонстрационных записей, чем загрузила эта установка: {{count}}.', - purgeConfirmAtMost_few: 'Будет удалено не больше демонстрационных записей, чем загрузила эта установка: {{count}}.', - purgeConfirmAtMost_many: 'Будет удалено не больше демонстрационных записей, чем загрузила эта установка: {{count}}.', - purgeConfirmAtMost_other: 'Будет удалено не больше демонстрационных записей, чем загрузила эта установка: {{count}}.', + purgeConfirmAtMost: 'Будет удалено не более {{count}} демонстрационных записей.', + purgeConfirmAtMost_one: 'Будет удалено не более {{count}} демонстрационной записи.', + purgeConfirmAtMost_few: 'Будет удалено не более {{count}} демонстрационных записей.', + purgeConfirmAtMost_many: 'Будет удалено не более {{count}} демонстрационных записей.', + purgeConfirmAtMost_other: 'Будет удалено не более {{count}} демонстрационных записей.', sampleDataBannerTitle: 'В «{{name}}» есть демоданные.', sampleDataBannerMessage: 'Они нужны, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', reseedQueued: 'Демоданные будут перезагружены при следующем доступе к среде.', diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index e9f150c4f97..69e8a2f6a10 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -4366,9 +4366,9 @@ const zh = { sampleDataLoaded_one: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', sampleDataLoaded_other: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', sampleDataReloaded: '再次安装会重新载入示例数据,包括已清除的示例数据。', - purgeConfirmAtMost: '最多删除 {{count}} 条示例记录,即本次安装载入的数量。', - purgeConfirmAtMost_one: '最多删除 {{count}} 条示例记录,即本次安装载入的数量。', - purgeConfirmAtMost_other: '最多删除 {{count}} 条示例记录,即本次安装载入的数量。', + purgeConfirmAtMost: '最多删除 {{count}} 条示例记录。', + purgeConfirmAtMost_one: '最多删除 {{count}} 条示例记录。', + purgeConfirmAtMost_other: '最多删除 {{count}} 条示例记录。', sampleDataBannerTitle: '"{{name}}" 中有示例数据。', sampleDataBannerMessage: '这些数据用于查看应用效果,正式使用前可以清除。', reseedQueued: '示例数据将在下次访问该环境时重新加载。', From 192d2d105cda5dcb6704f7554ecb93f744da1ebe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 19:26:09 +0000 Subject: [PATCH 3/4] chore(changeset): the package page's sample-data count after a reload (#30616) Claude-Session: https://claude.ai/code/session_01L6bhgTRFYJtEzJqfKEhD7C Co-authored-by: Claude --- .../30616-sample-data-count-after-reload.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/30616-sample-data-count-after-reload.md diff --git a/.changeset/30616-sample-data-count-after-reload.md b/.changeset/30616-sample-data-count-after-reload.md new file mode 100644 index 00000000000..fe8435e6650 --- /dev/null +++ b/.changeset/30616-sample-data-count-after-reload.md @@ -0,0 +1,15 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +--- + +feat(console): after a reload, the package page still says how many sample records a purge would delete + +Clause-②: no +Metadata-protocol: none + +- **The count on any visit.** The package page of a local install now reads the runtime's count of the sample rows a purge would delete right now (`GET /api/v1/marketplace/install-local/:manifestId/sample-data`), on every visit and again after an install, a reseed or a purge. Before, the purge confirm and the banner that follows the installer to the app knew a count only for an install made in the same browser session; a reload lost it. +- **The confirm.** The local purge confirm draws that count. When it cannot be read (refused, failed), it draws the count this session's install answered, as before. A read that began before an install, reseed or purge of this session answered is older than that answer and is not used. +- **The banner.** A count above zero raises the sample-data banner on leaving the page, as an install of this session does. A count of `0` drops what the session knew: no banner, and no count line in the confirm. +- **Types.** `marketplaceApi` gains `countLocalSampleData` and `LocalSampleDataCount`. +- **Strings.** `@object-ui/i18n` rewords `marketplace.detail.purgeConfirmAtMost` (all plural forms) in all 10 locales: it no longer says the count is "the number this install loaded", which is not what the runtime's count is. It stays an upper bound ("Up to N sample records will be deleted."), true of both counts. No key is added. From 5581ff5d498046190a4cc08ba398b4bb27ecda7c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 19:43:02 +0000 Subject: [PATCH 4/4] test(console): the marketplace page suites answer the sample-data count read (#30616) Mocks of `marketplaceApi` gain `countLocalSampleData`, and the offline local-menu suite unmounts before its fetch double goes, since a reseed or purge now re-reads the count after the test body returns. Claude-Session: https://claude.ai/code/session_01L6bhgTRFYJtEzJqfKEhD7C Co-authored-by: Claude --- .../__tests__/MarketplacePackagePage-5620.test.tsx | 1 + .../__tests__/MarketplacePackagePage.disabledState.test.tsx | 1 + .../MarketplacePackagePage.displayLocale-9909.test.tsx | 1 + .../__tests__/MarketplacePackagePage.guardOrder.test.tsx | 1 + .../MarketplacePackagePage.offlineLocalMenu-11627.test.tsx | 6 +++++- .../MarketplacePackagePage.readmeWidth-30330.test.tsx | 1 + .../MarketplacePackagePage.releaseNotes-28891.test.tsx | 1 + .../MarketplacePackagePage.sampleDataRefusal-10432.test.tsx | 1 + .../MarketplacePackagePage.servedTruth-12098.test.tsx | 1 + .../MarketplacePackagePage.updateDialog-12146.test.tsx | 1 + ...ketplacePackagePage.updateIsHigherVersion-10899.test.tsx | 1 + .../MarketplacePackagePage.waitServedApp-12087.test.tsx | 1 + .../__tests__/MarketplacePage.guardOrder.test.tsx | 1 + .../__tests__/MarketplaceRefusalResolution-5619.test.tsx | 1 + 14 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage-5620.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage-5620.test.tsx index efe99b44756..bd8f0ec3961 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage-5620.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage-5620.test.tsx @@ -102,6 +102,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.disabledState.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.disabledState.test.tsx index 7055c3adc4c..f2d0f1924c3 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.disabledState.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.disabledState.test.tsx @@ -77,6 +77,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.displayLocale-9909.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.displayLocale-9909.test.tsx index 5a3b8428271..d0dd428dcca 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.displayLocale-9909.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.displayLocale-9909.test.tsx @@ -64,6 +64,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.guardOrder.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.guardOrder.test.tsx index 78207876ed9..182fc14bffa 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.guardOrder.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.guardOrder.test.tsx @@ -104,6 +104,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.offlineLocalMenu-11627.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.offlineLocalMenu-11627.test.tsx index c52d67dcf6e..b08f5dfd1e8 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.offlineLocalMenu-11627.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.offlineLocalMenu-11627.test.tsx @@ -39,7 +39,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { act, render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { act, cleanup, render, screen, fireEvent, waitFor } from '@testing-library/react'; const route = vi.hoisted(() => ({ packageId: 'com.example.crm' })); const viewer = vi.hoisted(() => ({ isAdmin: true })); @@ -162,6 +162,10 @@ beforeEach(() => { }); afterEach(() => { + // Unmount BEFORE the fetch double goes: a reseed or purge re-reads the + // ledger and then the sample-data count (objectstack#30616) after the test + // body returns, and a read issued once the double is gone reaches a socket. + cleanup(); vi.unstubAllGlobals(); resetRuntimeConfigForTesting(); }); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.readmeWidth-30330.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.readmeWidth-30330.test.tsx index 33f99bf46df..aa97c5b3ddd 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.readmeWidth-30330.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.readmeWidth-30330.test.tsx @@ -61,6 +61,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); // The runtime serves the installed version at once: the wait answers served. diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.releaseNotes-28891.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.releaseNotes-28891.test.tsx index ba055e07ed4..2e6066ecb01 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.releaseNotes-28891.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.releaseNotes-28891.test.tsx @@ -58,6 +58,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); // The runtime serves the installed version at once: the wait answers served. diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataRefusal-10432.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataRefusal-10432.test.tsx index d4cc17b8374..2b6d191f5ae 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataRefusal-10432.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataRefusal-10432.test.tsx @@ -78,6 +78,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: (...a: unknown[]) => purgeSampleData(...a), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.servedTruth-12098.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.servedTruth-12098.test.tsx index 63c27aeab7e..6f8546e0ec5 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.servedTruth-12098.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.servedTruth-12098.test.tsx @@ -120,6 +120,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); // Mounts its own read of the runtime; here it only marks WHEN it mounts. diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateDialog-12146.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateDialog-12146.test.tsx index 5ea94399d8f..a52ca948d45 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateDialog-12146.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateDialog-12146.test.tsx @@ -67,6 +67,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); // The runtime serves the installed version at once: the wait answers served. diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateIsHigherVersion-10899.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateIsHigherVersion-10899.test.tsx index 6841e6da9f3..c2b2f1d6e3f 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateIsHigherVersion-10899.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.updateIsHigherVersion-10899.test.tsx @@ -58,6 +58,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.waitServedApp-12087.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.waitServedApp-12087.test.tsx index 86762e9e573..f840953ee9f 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.waitServedApp-12087.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.waitServedApp-12087.test.tsx @@ -82,6 +82,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); // Mounts its own read of the runtime; here it only marks WHEN it mounts. diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.guardOrder.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.guardOrder.test.tsx index 2c4d8637b39..0b576fe7cce 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.guardOrder.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.guardOrder.test.tsx @@ -103,6 +103,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplaceRefusalResolution-5619.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplaceRefusalResolution-5619.test.tsx index 0727a1817e7..6b2e2b0df6b 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplaceRefusalResolution-5619.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplaceRefusalResolution-5619.test.tsx @@ -82,6 +82,7 @@ vi.mock('../marketplaceApi', () => ({ purgeSampleData: async () => ({ ok: true }), reseedLocalSampleData: async () => ({ ok: true }), purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, })); vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));