Skip to content

Commit 47c457b

Browse files
feat(console): after a reload, the package page still says how many sample records a purge would delete (#30653)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu562dBE8bEQM37ezqErQj94)_ domain:ui#6 · os-sales · session_01L6bhgTRFYJtEzJqfKEhD7C Fixes #30616 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:已装应用的详情页每次打开都会去读服务端 #30613 新接口给出的「此刻清除会删几条示例记录」,清除确认框和离开页面后的横幅都用这个数;刷新页面后不再丢失。 - 为什么改:#30537 只在本次浏览器会话里装过才知道条数,刷新后确认框说不出会删几条,横幅也不再出现。 - 风险与回滚:只多一个只读 GET,不碰清除逻辑和匹配规则;读不到时退回原来的会话内数字。确认框那句文案在 10 种语言里去掉了「即本次安装载入的数量」半句(键不变,没有新增键,首屏 en 包 gzip 反而少 9 字节)。回滚即还原本 PR。 - 先看:`MarketplacePackagePage.tsx` 新增的读数 effect 与 `localSampleCount`;`sampleDataNotice.ts` 的 `noteServedSampleCount` 和代次判断;`en.ts` 的 `purgeConfirmAtMost`。 - 两分钟验证:`cd ui && pnpm exec vitest run packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataNotice-30323.test.tsx`,预期 20 个全绿,其中 #30616 两个 describe 共 8 个。 - 需要你做的:无,席位落地。 ## What changes PR #30613 added `GET /api/v1/marketplace/install-local/:manifestId/sample-data`, which answers how many rows a sample-data purge would delete right now, counted with the purge's own matcher and keep-rules. The console only knew a count for an install made in the same browser session (PR #30537); after a reload the purge confirm had no count and the banner that follows the installer to the app was gone. - **`marketplaceApi.countLocalSampleData(manifestId)`** reads that route and answers `{ manifestId, count, unidentified }`, or `null` when the read is refused, fails, or carries no non-negative integer `count`. - **The package page reads the count on every visit** of a local install the runtime loaded (not for a `notLoaded` entry), and again every time the install-local listing is read, which already happens after an install, a reseed and a purge. The answer goes into the existing session record (`sampleDataNotice.ts`) through the new `noteServedSampleCount`: - a count above zero updates the record in place, or starts one when this session had none (a standing banner and a pending confirm request stay with it); - `count: 0` drops the record and withdraws its banner. - **Freshness.** A read that began before an install, reseed or purge of this session answered is older than that answer and is ignored: the record keeps a per-package generation that those three answers bump, and the read compares the generation it saw when it started. The effect's cleanup also drops a superseded read. A failed read changes nothing, so the confirm keeps the count this session's install answered. - **The confirm** now reads its count when it is drawn (`localSampleCount`), so a count that lands while it is open reaches it; the state keeps only the target. A cloud purge draws no count, as before. - **The banner** needs no change of its own: it is raised on leaving from the session record, so a served count above zero raises it after a reload too, and `count: 0` leaves nothing to raise. - **Copy, no new key.** `marketplace.detail.purgeConfirmAtMost` said the count was "the number this install loaded", false for the runtime's count. All 10 packs drop that clause and keep the upper bound, e.g. en `Up to {{count}} sample records will be deleted.`, zh `最多删除 {{count}} 条示例记录。`, ru reworded to `не более {{count}} …` with the singular-genitive `_one`. The upper bound holds for both sources: the served count is exact, and the session count is at most what a purge deletes. The eager `en` pack shrinks (see Verification); `#30588`'s ceiling is untouched. `unidentified` is read but not drawn: saying it needs a new string, which the eager `en` pack has no room for until #30588 moves strings to a lazy namespace. ## Tests `ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.sampleDataNotice-30323.test.tsx` is this surface's shared harness, so the new pins are two top-level `describe`s there (one stubbed `fetch` routed by path; the real `marketplaceApi`): - a visit with no install this session reads the count once and the confirm draws `count=42`; - that count raises the banner on leaving; - `count: 0` drops a record the session held: no count line in the confirm, no banner on leaving; - after an in-session install of 369, the re-read count 360 is the one the confirm draws, and the install note keeps 369; - CONTROL: a refused read (403) keeps the install's 369; - CONTROL: a `notLoaded` entry is never counted; - unit: a read whose generation predates an install's answer does not replace it; one taken after does, and `0` drops the record. Support edits: 13 suites that mock `../marketplaceApi` gain `countLocalSampleData: async () => null`, and the offline local-menu suite (`…offlineLocalMenu-11627.test.tsx`) now calls `cleanup()` before `vi.unstubAllGlobals()`, because a reseed or purge re-reads the count after the test body returns and that read reached a socket once the double was gone. ## Acceptance notes - Every listing read now brings one more GET (the count). The listing is read on visit and after install, reseed and purge only. - A visit to the package page of an app holding sample data now raises the leave banner even when this session did not install it. That is the card's ask ("read the count on any visit"); the banner is dismissable and a purge withdraws it. - `unidentified` is not drawn (no string budget, see above). - **Merge order with PR #30640** (lane card #30588, which moves the marketplace strings out of the eager packs into a lazy `marketplace` namespace): this PR changes the values of the `purgeConfirmAtMost` family in `ui/packages/i18n/src/locales/*.ts`. Whichever of the two lands second carries the new values into the namespace files; if PR #30640 lands first, this branch merges `main` and moves the change there. ## Verification <details><summary>Ablation: the count read removed, 5 of the new pins go red</summary> `node scripts/ablation-replace.mjs --file ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx --anchor 'const served = await countLocalSampleData(manifestId);' --replacement 'const served = null as …;' -- …vitest run …sampleDataNotice-30323.test.tsx`: anchor 1 → 0, blob `3ab826d5e075 → 87d8ee093b8e`; `Tests 5 failed | 15 passed (20)` (the five page-level #30616 pins); restored, blob == HEAD and `git diff HEAD` empty. The app-shell suite reads this page from `src/` (relative import), so no build sits between the mutation and the run. </details> <details><summary>Eager en pack, gzip bytes (the CI Build &amp; E2E UI ceiling is 60,000)</summary> Console built from this branch (`turbo run build --filter='@object-ui/console^...'`, then `CI=true vite build` in `ui/apps/console`); `dist/eager-closure.json` reads `assets/i18n-locale-en-*.js` at 192,979 raw / **59,942** gzip. The same chunk with the three old `purgeConfirmAtMost` strings swapped back in, re-gzipped with `zlib.gzipSync` (level 6, what the report uses): 193,081 raw / **59,951** gzip. So −9 B gzip, −102 B raw. `node scripts/check-eager-closure-budget.mjs` (in `ui/`): exit 0. </details> <details><summary>Local tests and gates</summary> All on head `eef04e97` (this branch merged with `main` at `af8aab1b`) unless noted. Lock readings are the lock's own VERDICT lines. - `pnpm --dir ui exec vitest run packages/i18n/ packages/app-shell/src/console/marketplace/` (after rebuilding `@object-ui/app-shell^...` on the merged tree): Test Files 146 passed (146), Tests 1898 passed, 13 skipped. - `pnpm --filter @object-ui/app-shell run type-check` (source and tests): exit 0, on `5581ff5d` (the merge brought no file under `ui/packages/app-shell` or `ui/packages/i18n`). - `pnpm --dir ui exec vitest run scripts/`: 170 files passed, 2 skipped; 4970 tests passed. - `node ui/scripts/check-i18n-en-drift.mjs --base origin/main`: exit 0, every changed en value followed by all nine packs. `pnpm --dir ui run check:i18n-keys`: exit 0 (4,210 keys). `node ui/scripts/check-eager-closure-budget.mjs`: exit 0 (on `5581ff5d`'s console build). - eslint on every changed source and test: 0 errors; the warnings are pre-existing `no-explicit-any` / one unused var in the page. - `node scripts/pm/dispatch-gates.mjs --commands`: 35 derived families, every one exit 0; `--ran` reconciles 35 run, 0 NOT-MEASURED, 0 UNRUN. - **NOT MEASURED: the whole `@object-ui/app-shell` suite** (2,303 test files; stopped after 40 minutes at `--maxWorkers=2` on a shared box). The local run is narrowed to the marketplace directory and the i18n package, the two trees whose files this diff changes or whose mocks name the API it extends; `Test UI` runs the rest. **Live stack** (showcase `objectstack dev --fresh`, the fake marketplace control plane from #30325's rig, console via vite; evidence in the project's shared files, not committed): - curl, rig package with 3 seed records: count 3 after install, still 3 after a user adds a record of their own, purge answers `deleted: 3`, count 0 after, the user's record stays. - Browser, a 13-record package installed through the API and the package page then opened fresh (no install answer in this browser session): two `GET …/sample-data` reads; the ⋯ menu's purge confirm reads `Up to 13 sample records will be deleted.`; leaving the page by its back link raises `"Acme Deals" has sample data.` with the purge action. After a purge (`deleted: 13`, count 0), a fresh visit and leave raises no banner. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgTRFYJtEzJqfKEhD7C)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fef212b commit 47c457b

29 files changed

Lines changed: 325 additions & 60 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(console): after a reload, the package page still says how many sample records a purge would delete
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **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.
12+
- **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.
13+
- **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.
14+
- **Types.** `marketplaceApi` gains `countLocalSampleData` and `LocalSampleDataCount`.
15+
- **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.

‎ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx‎

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
purgeSampleData,
8282
reseedLocalSampleData,
8383
purgeLocalSampleData,
84+
countLocalSampleData,
8485
type MarketplaceDetailResponse,
8586
type CloudEnvironment,
8687
type LocalInstallEntry,
@@ -92,7 +93,15 @@ import {
9293
import { useDependencyMissingText } from './dependencyMissing.js';
9394
import { useProtocolCheckSkippedText } from './protocolCheckSkipped.js';
9495
import { ProtocolCheckSkippedNote } from './ProtocolCheckSkippedNote.js';
95-
import { forgetSampleData, rememberSampleData, sampleDataNotice, sampleRecordsLoaded, takeClearConfirmRequest } from './sampleDataNotice.js';
96+
import {
97+
forgetSampleData,
98+
noteServedSampleCount,
99+
rememberSampleData,
100+
sampleDataGeneration,
101+
sampleDataNotice,
102+
sampleRecordsLoaded,
103+
takeClearConfirmRequest,
104+
} from './sampleDataNotice.js';
96105
import { SampleDataLeaveBanner } from './SampleDataLeaveBanner.js';
97106
import { getCloudBase, getRuntimeConfig, isMarketplaceEnabled } from '../../runtime-config.js';
98107
import { emitMetadataRefresh } from '../../assistant/assistantBus.js';
@@ -289,9 +298,13 @@ export function MarketplacePackagePage() {
289298
sampleData?: { count: number; reinstall: boolean };
290299
} | null>(null);
291300
// objectstack#30323 — the console's own confirm for a sample-data purge, in
292-
// place of the browser's: which install it purges, and how many sample
293-
// records this session knows that install loaded (`null`: none known).
294-
const [purgeConfirm, setPurgeConfirm] = useState<{ target: 'local' | 'cloud'; count: number | null } | null>(null);
301+
// place of the browser's: which install it purges. The count it draws for a
302+
// local purge is read when it is drawn (`localSampleCount` below), so a
303+
// count the runtime answers while it is open still reaches it.
304+
const [purgeConfirm, setPurgeConfirm] = useState<{ target: 'local' | 'cloud' } | null>(null);
305+
// objectstack#30616 — bumped when the runtime's count of this page's local
306+
// install's sample rows lands in the session record, so the page draws it.
307+
const [, setSampleCountReads] = useState(0);
295308
const hasNotificationProvider = useHasNotificationProvider();
296309
// ADR-0131 D6 — the install modes the package's latest manifest declares,
297310
// read from the manifest install-local judges, and the operator's pick among
@@ -331,7 +344,7 @@ export function MarketplacePackagePage() {
331344
// page it opens, so the confirm opens once the listing says that
332345
// package's sample data is still there.
333346
const requested = items.find((i) => i.withSampleData && !i.notLoaded && takeClearConfirmRequest(i.manifestId));
334-
if (requested) setPurgeConfirm({ target: 'local', count: sampleDataNotice(requested.manifestId)?.count ?? null });
347+
if (requested) setPurgeConfirm({ target: 'local' });
335348
})();
336349
return () => { cancelled = true; };
337350
}, [packageId, localResult, isAdmin]);
@@ -898,12 +911,36 @@ export function MarketplacePackagePage() {
898911
? localInstalls.find((i) => !!data && i.manifestId === data.package.manifest_id)
899912
: localInstalls.find((i) => i.packageId === packageId)) ?? null;
900913

914+
// objectstack#30616 — the runtime's count of the sample rows a purge of this
915+
// install would delete now, read on every visit and again each time the
916+
// listing is read (after an install, a reseed or a clear, which hand the
917+
// page a new entry). It goes into the session record the confirm and the
918+
// banner read: above zero it is the count they draw, zero drops the record
919+
// and its banner. An answer of this session's own that lands while the read
920+
// is in flight is fresher and stays (`noteServedSampleCount`). A failed read
921+
// changes nothing: the record keeps what this session knows, if anything.
922+
useEffect(() => {
923+
if (!localInstall || localInstall.notLoaded) return;
924+
const manifestId = localInstall.manifestId;
925+
const generation = sampleDataGeneration(manifestId);
926+
let cancelled = false;
927+
(async () => {
928+
const served = await countLocalSampleData(manifestId);
929+
if (cancelled || !served) return;
930+
const name = data?.package ? localizePackage(data.package as any, language).displayName : manifestId;
931+
noteServedSampleCount({ manifestId, name, count: served.count, path: `${basePath}/system/marketplace/${packageId}` }, generation);
932+
setSampleCountReads((n) => n + 1);
933+
})();
934+
return () => { cancelled = true; };
935+
}, [localInstall, data, language, basePath, packageId]);
936+
901937
// objectstack#30323 — the console's confirm for a local purge, with the
902-
// count this session knows the install left in place, when it knows one.
938+
// count this session knows the install's purge would delete, when it knows one.
903939
const openLocalPurgeConfirm = () => {
904940
if (!localInstall) return;
905-
setPurgeConfirm({ target: 'local', count: sampleDataNotice(localInstall.manifestId)?.count ?? null });
941+
setPurgeConfirm({ target: 'local' });
906942
};
943+
const localSampleCount = localInstall ? sampleDataNotice(localInstall.manifestId)?.count ?? null : null;
907944

908945
// The local-install menu and the two status banners, drawn by both the
909946
// catalog view and the marketplace-off local view below (objectui#11627).
@@ -996,16 +1033,18 @@ export function MarketplacePackagePage() {
9961033
</div>
9971034
);
9981035
// objectstack#30323 — the purge confirm, in the console's own dialog. The
999-
// count line is drawn only when this session knows what the install loaded:
1000-
// the runtime serves no count of a package's sample rows after that answer.
1036+
// count line is drawn for a local purge whose count this session knows: the
1037+
// runtime's (objectstack#30616) or, when that could not be read, the one the
1038+
// install answered. A cloud purge draws none.
1039+
const purgeConfirmCount = purgeConfirm?.target === 'local' ? localSampleCount : null;
10011040
const purgeConfirmDialog = (
10021041
<AlertDialog open={purgeConfirm !== null} onOpenChange={(open) => { if (!open) setPurgeConfirm(null); }}>
10031042
<AlertDialogContent data-testid="purge-sample-data-dialog">
10041043
<AlertDialogHeader>
10051044
<AlertDialogTitle>{t('marketplace.detail.purgeSampleData')}</AlertDialogTitle>
10061045
<AlertDialogDescription className="whitespace-pre-line">
1007-
{purgeConfirm?.count != null
1008-
? `${t('marketplace.detail.purgeConfirmAtMost', { count: purgeConfirm.count })}\n${t('marketplace.detail.purgeConfirm')}`
1046+
{purgeConfirmCount != null
1047+
? `${t('marketplace.detail.purgeConfirmAtMost', { count: purgeConfirmCount })}\n${t('marketplace.detail.purgeConfirm')}`
10091048
: t('marketplace.detail.purgeConfirm')}
10101049
</AlertDialogDescription>
10111050
</AlertDialogHeader>
@@ -1509,7 +1548,7 @@ export function MarketplacePackagePage() {
15091548
: t('marketplace.detail.addSampleData')}
15101549
</DropdownMenuItem>
15111550
<DropdownMenuItem
1512-
onSelect={() => setPurgeConfirm({ target: 'cloud', count: null })}
1551+
onSelect={() => setPurgeConfirm({ target: 'cloud' })}
15131552
disabled={sampleDataBusy !== null || cloudSampleDataRefused || !cloudInstall.withSampleData}
15141553
className="text-destructive focus:text-destructive"
15151554
>

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage-5620.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ vi.mock('../marketplaceApi', () => ({
102102
purgeSampleData: async () => ({ ok: true }),
103103
reseedLocalSampleData: async () => ({ ok: true }),
104104
purgeLocalSampleData: async () => ({ ok: true }),
105+
countLocalSampleData: async () => null,
105106
}));
106107

107108
vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.disabledState.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ vi.mock('../marketplaceApi', () => ({
7777
purgeSampleData: async () => ({ ok: true }),
7878
reseedLocalSampleData: async () => ({ ok: true }),
7979
purgeLocalSampleData: async () => ({ ok: true }),
80+
countLocalSampleData: async () => null,
8081
}));
8182

8283
vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.displayLocale-9909.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ vi.mock('../marketplaceApi', () => ({
6464
purgeSampleData: async () => ({ ok: true }),
6565
reseedLocalSampleData: async () => ({ ok: true }),
6666
purgeLocalSampleData: async () => ({ ok: true }),
67+
countLocalSampleData: async () => null,
6768
}));
6869

6970
vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.guardOrder.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ vi.mock('../marketplaceApi', () => ({
104104
purgeSampleData: async () => ({ ok: true }),
105105
reseedLocalSampleData: async () => ({ ok: true }),
106106
purgeLocalSampleData: async () => ({ ok: true }),
107+
countLocalSampleData: async () => null,
107108
}));
108109

109110
vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.offlineLocalMenu-11627.test.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
import '@testing-library/jest-dom/vitest';
4141
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
42-
import { act, render, screen, fireEvent, waitFor } from '@testing-library/react';
42+
import { act, cleanup, render, screen, fireEvent, waitFor } from '@testing-library/react';
4343

4444
const route = vi.hoisted(() => ({ packageId: 'com.example.crm' }));
4545
const viewer = vi.hoisted(() => ({ isAdmin: true }));
@@ -162,6 +162,10 @@ beforeEach(() => {
162162
});
163163

164164
afterEach(() => {
165+
// Unmount BEFORE the fetch double goes: a reseed or purge re-reads the
166+
// ledger and then the sample-data count (objectstack#30616) after the test
167+
// body returns, and a read issued once the double is gone reaches a socket.
168+
cleanup();
165169
vi.unstubAllGlobals();
166170
resetRuntimeConfigForTesting();
167171
});

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.readmeWidth-30330.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ vi.mock('../marketplaceApi', () => ({
6161
purgeSampleData: async () => ({ ok: true }),
6262
reseedLocalSampleData: async () => ({ ok: true }),
6363
purgeLocalSampleData: async () => ({ ok: true }),
64+
countLocalSampleData: async () => null,
6465
}));
6566

6667
// The runtime serves the installed version at once: the wait answers served.

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.releaseNotes-28891.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ vi.mock('../marketplaceApi', () => ({
5858
purgeSampleData: async () => ({ ok: true }),
5959
reseedLocalSampleData: async () => ({ ok: true }),
6060
purgeLocalSampleData: async () => ({ ok: true }),
61+
countLocalSampleData: async () => null,
6162
}));
6263

6364
// The runtime serves the installed version at once: the wait answers served.

0 commit comments

Comments
 (0)