diff --git a/.changeset/11627-offline-local-package-menu.md b/.changeset/11627-offline-local-package-menu.md new file mode 100644 index 0000000000..2f6f9c1fe1 --- /dev/null +++ b/.changeset/11627-offline-local-package-menu.md @@ -0,0 +1,11 @@ +--- +'@object-ui/app-shell': patch +--- + +On a runtime with no marketplace that still mounts install-local (an offline boot, `OS_CLOUD_URL=off`), a local install's Details page now offers its local menu: re-seed sample data, purge sample data and uninstall from this runtime (objectui#11627). + +Installed Apps links each local install's Details to the marketplace package page. That page answered a marketplace-off runtime with the "App Marketplace is turned off" notice and nothing else, so the local menu below that check could not be reached and no install-local request was ever issued. Now, when the runtime config reports `features.installLocal` and the viewer is an admin, the page draws the package's header (manifest id and installed version) and its local menu above the same notice. The menu issues the same `POST /api/v1/marketplace/install-local/MANIFEST_ID/reseed-sample-data` and `.../purge-sample-data` calls as on a marketplace-on runtime. The page finds the install by the id Installed Apps put in the link (the ledger entry's `packageId`), and the menu acts on its manifest id. + +Everything that needs a marketplace stays refused as before: the page sends no catalog request and no cloud-installation probe, and it draws no install-to-cloud button, readme or version list. A viewer who is not an admin, a runtime that does not mount install-local, and a package that is not a local install all see the notice alone, as before. + +**Clause-②: no.** Nothing on the package entry changes: no export, prop, type member or i18n key is added. `MarketplacePackagePage` takes no props, and the strings it draws already existed. diff --git a/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx b/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx index 592a6c2375..e06fd02976 100644 --- a/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx +++ b/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx @@ -166,13 +166,13 @@ export function MarketplacePackagePage() { // with no marketplace proxy at all), so it stays its own check rather than // being folded into the other two predicates. if (!getRuntimeConfig().features.installLocal) return; - // Its only consumer is `localInstalls.find(...)` in the content branch - // below, which is unreachable whenever the page has already returned - // `MarketplaceDisabled` or (post-objectui#5583) `MarketplaceAccessDenied`. - // Firing anyway would be the same discarded-request class `2573ff434` - // closed for this page, on the flag that change was not about - // (objectui#5620). - if (!marketplaceEnabled) return; + // Its only consumer is the `localInstall` match below, read by the content + // branch AND by the marketplace-off local view (objectui#11627). Neither + // is drawn for a viewer refused the admin surface, so firing for one would + // be the same discarded-request class `2573ff434` closed for this page + // (objectui#5620). `marketplaceEnabled` is deliberately NOT a gate any + // more: on a runtime with no marketplace but a mounted install-local + // surface, this answer is what decides whether the local menu is drawn. if (!isAdmin) return; let cancelled = false; (async () => { @@ -180,7 +180,7 @@ export function MarketplacePackagePage() { if (!cancelled) setLocalInstalls(items); })(); return () => { cancelled = true; }; - }, [packageId, localResult, marketplaceEnabled, isAdmin]); + }, [packageId, localResult, isAdmin]); // Seed cloud-install state so the primary CTA renders as "Installed" on // first paint instead of inviting another install. @@ -555,6 +555,103 @@ export function MarketplacePackagePage() { } }; + // `features.installLocal` is the server's own answer to "is the install-local + // surface mounted here" -- the same read the effect above gates on, never a + // guess from the package's fields. + const supportsLocal = getRuntimeConfig().features.installLocal; + // Which local install, if any, this page is about. With a marketplace the + // catalog row names it, by manifest id. Without one there is no catalog row, + // and the route's id is the one Installed Apps built this link from + // (`entry.packageId`), so it is matched on that same field. Declared ahead of + // every early return: the local sample-data handlers above close over it. + const localInstall = (marketplaceEnabled + ? localInstalls.find((i) => !!data && i.manifestId === data.package.manifest_id) + : localInstalls.find((i) => i.packageId === packageId)) ?? 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). + const localInstalledBadge = localInstall && ( + + + ); + const localMenu = localInstall && ( + + + + + + + {sampleDataBusy === 'reseed' + ? + + {sampleDataBusy === 'purge' + ? + + + + + ); + const sampleDataNote = sampleDataMsg && ( +
+ {sampleDataMsg.ok ?
+ ); + // `suggestFor` is the manifest id whose suggested audience bindings a + // successful local INSTALL surfaces (ADR-0090 D5), or `null` where no install + // door is drawn and the only result this banner can carry is an uninstall's. + const localResultNote = (suggestFor: string | null) => localResult && ( +
+ {localResult.ok ?
+ ); + // A CONFIGURATION CONCLUSION, not a load failure -- the same informational // state the catalog page renders, so the two pages stop disagreeing about the // same runtime (`2573ff434`). Reached by a pasted or bookmarked package URL, @@ -565,7 +662,45 @@ export function MarketplacePackagePage() { // `features.marketplace` is public runtime config that any client already // reads. Telling an unprivileged operator they lack permission for a surface // that exists for nobody is the same misdirection this fix removes. - if (!marketplaceEnabled) return ; + // + // objectui#11627 — that answer covers the REMOTE half of the page only. A + // runtime with no marketplace can still mount the install-local surface (an + // offline boot, `OS_CLOUD_URL=off`), and Installed Apps links each local + // install's Details here; the notice used to be all that page showed, so + // local reseed / purge had no way in. Now an admin whose package IS a local + // install also gets its header and local menu, ABOVE the same notice: the + // catalog fetch, the install-to-cloud CTA, the readme and the version list + // stay refused exactly as before, and every other viewer sees the notice + // alone. The local half is ADDED once `listLocalInstalls` answers, so the + // notice is never painted and then retracted. + if (!marketplaceEnabled) { + if (!(supportsLocal && isAdmin && (localInstall || localResult))) return ; + return ( +
+ {localInstall && ( +
+ +
+

{localInstall.manifestId}

+
+ {localInstalledBadge} +
+
+
+ {localMenu} +
+
+ )} + {sampleDataNote} + {localResultNote(null)} + +
+ ); + } // Ahead of BOTH the loading and the load-failure branches below // (objectui#5583): authorization is not a function of whether the fetch @@ -630,7 +765,6 @@ export function MarketplacePackagePage() { const pkg = data.package; const loc = localizePackage(pkg as any, language); const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null; - const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null; // PD4 (ADR-0025 §3.11): code-bearing packages must disclose + be acknowledged. const containsCode = !!pkg.latest_version?.contains_code; // ADR-0010 version lifecycle: installed cloud env is on an OLDER version than @@ -643,7 +777,6 @@ export function MarketplacePackagePage() { && !!latestVersion && isNewerVersion(latestVersion, cloudInstalledVersion); - const supportsLocal = getRuntimeConfig().features.installLocal; const primaryDisabled = !latestVersion || installingLocal || installing || (!supportsLocal && !!cloudInstalledVersion && !cloudUpdateAvailable); const primaryAction = supportsLocal ? { @@ -713,12 +846,7 @@ export function MarketplacePackagePage() { )} {categoryLabel && {categoryLabel}} {pkg.license && {pkg.license}} - {localInstall && ( - - - )} + {localInstalledBadge} {!localInstall && cloudInstalledVersion && (