Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/30616-sample-data-count-after-reload.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
purgeSampleData,
reseedLocalSampleData,
purgeLocalSampleData,
countLocalSampleData,
type MarketplaceDetailResponse,
type CloudEnvironment,
type LocalInstallEntry,
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -996,16 +1033,18 @@ export function MarketplacePackagePage() {
</div>
);
// 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 = (
<AlertDialog open={purgeConfirm !== null} onOpenChange={(open) => { if (!open) setPurgeConfirm(null); }}>
<AlertDialogContent data-testid="purge-sample-data-dialog">
<AlertDialogHeader>
<AlertDialogTitle>{t('marketplace.detail.purgeSampleData')}</AlertDialogTitle>
<AlertDialogDescription className="whitespace-pre-line">
{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')}
</AlertDialogDescription>
</AlertDialogHeader>
Expand Down Expand Up @@ -1509,7 +1548,7 @@ export function MarketplacePackagePage() {
: t('marketplace.detail.addSampleData')}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => setPurgeConfirm({ target: 'cloud', count: null })}
onSelect={() => setPurgeConfirm({ target: 'cloud' })}
disabled={sampleDataBusy !== null || cloudSampleDataRefused || !cloudInstall.withSampleData}
className="text-destructive focus:text-destructive"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {} }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {} }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {} }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {} }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down Expand Up @@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading