@aglyn/plugins-marketplace 1.0.0-beta.253 → 1.0.0-beta.255
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/org-plugin-installs-card.component.js +2 -1
- package/src/lib/components/org-plugin-installs-card.component.js.map +1 -1
- package/src/lib/components/plugin-site-set.component.js +19 -15
- package/src/lib/components/plugin-site-set.component.js.map +1 -1
- package/src/lib/components/uninstall-impact-dialog.component.js +35 -55
- package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -1
- package/src/lib/hooks/use-marketplace-actions.js +17 -22
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
- package/src/lib/model/marketplace-catalog.d.ts +288 -0
- package/src/lib/model/marketplace-catalog.js +447 -1
- package/src/lib/model/marketplace-catalog.js.map +1 -1
- package/src/lib/model/marketplace.d.ts +2 -70
- package/src/lib/model/marketplace.js +1 -65
- package/src/lib/model/marketplace.js.map +1 -1
- package/src/lib/model/update-state.d.ts +2 -45
- package/src/lib/model/update-state.js +2 -57
- package/src/lib/model/update-state.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/uninstall-impact-dialog.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { UninstallTarget } from '../model/marketplace'\n\n/** Mirrors the `/api/hosts/plugin-impact` response. */\ninterface ImpactSite {\n hostId: string\n label: string\n stillCovered: boolean\n placements: Array<{\n type: 'screen' | 'layout' | 'component'\n id: string\n name: string\n count: number\n }>\n affectedScreens: number\n truncated: boolean\n}\ninterface Impact {\n sites: ImpactSite[]\n losingSites: number\n placements: number\n affectedScreens: number\n truncated: boolean\n dataSurvives: boolean\n}\n\n/** The word people expect to type when a confirmation is worth slowing down. */\nconst ACKNOWLEDGEMENT = 'UNINSTALL'\n\nconst PLACEMENT_NOUNS: Record<ImpactSite['placements'][number]['type'], string> =\n {\n screen: 'page',\n layout: 'layout',\n component: 'component',\n }\n\nexport interface UninstallImpactDialogProps {\n open: boolean\n listing: { $id?: string; displayName?: string; pluginId?: string } | null\n /** Sites this uninstall touches — see `resolveUninstallTargets`. */\n targets: readonly UninstallTarget[]\n scope: 'org' | 'host'\n onCancel: () => void\n onConfirm: () => void | Promise<unknown>\n}\n\n/**\n * What an uninstall breaks, before it happens (AGL-1027).\n *\n * Uninstalling was a single click with no confirmation and no statement of\n * consequence — the pin went away and every page placing the plugin stopped\n * rendering it, on live sites, silently. A generic \"are you sure?\" would not\n * have fixed that: a confirmation that only asks whether you are sure trains\n * people to click through it. This one names what stops working, and scales its\n * friction to the finding:\n *\n * * nothing placed anywhere → a plain confirm, because there is nothing to\n * weigh and adding ceremony to a harmless action is how ceremony stops\n * being read;\n * * published pages affected → a typed acknowledgement, because the number is\n * the point and typing it is the only way to be sure it was seen.\n *\n * It also has to say when uninstalling changes NOTHING visible: removing a host\n * pin while an org pin still covers the site swaps which pointer is in use, and\n * a dialog that implied the plugin was about to stop working would be wrong in\n * the direction that stops people trusting it.\n */\nexport function UninstallImpactDialog(props: UninstallImpactDialogProps) {\n const { open, listing, targets, scope, onCancel, onConfirm } = props\n const { data: user } = useUser()\n const [impact, setImpact] = useState<Impact | null>(null)\n const [failed, setFailed] = useState(false)\n const [typed, setTyped] = useState('')\n const [busy, setBusy] = useState(false)\n\n const pluginId = listing?.pluginId ? String(listing.pluginId) : ''\n const losing = targets.filter((target) => !target.stillCovered)\n const covered = targets.filter((target) => target.stillCovered)\n const targetKey = targets.map((target) => target.hostId).join(',')\n\n useEffect(() => {\n if (!open) {\n setImpact(null)\n setFailed(false)\n setTyped('')\n return\n }\n // Nothing to scan: either the plugin has no manifest id to match nodes on,\n // or every site keeps it. Both are answerable without a request.\n if (!pluginId || !losing.length) {\n setImpact(null)\n return\n }\n let active = true\n setFailed(false)\n void (async () => {\n try {\n const response = await authorizedFetch(\n user,\n '/api/hosts/plugin-impact',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n pluginId,\n hostIds: targets.map((target) => target.hostId),\n stillCoveredHostIds: covered.map((target) => target.hostId),\n }),\n },\n )\n const payload = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok || !payload) return void setFailed(true)\n setImpact(payload as Impact)\n } catch {\n if (active) setFailed(true)\n }\n })()\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, pluginId, targetKey, user])\n\n const confirm = useCallback(async () => {\n setBusy(true)\n try {\n await onConfirm()\n } finally {\n setBusy(false)\n }\n }, [onConfirm])\n\n const name = listing?.displayName ?? 'this plugin'\n const scanning = Boolean(pluginId) && losing.length > 0 && !impact && !failed\n const affected = impact?.affectedScreens ?? 0\n /**\n * The scan could not speak, so it must not be read as \"nothing breaks\".\n * A failed check is a reason for MORE friction, not less — the one thing it\n * rules out is confidence.\n */\n const unknown = failed || (!pluginId && losing.length > 0)\n const needsAcknowledgement = affected > 0 || unknown\n const canConfirm =\n !busy &&\n !scanning &&\n (!needsAcknowledgement || typed.trim().toUpperCase() === ACKNOWLEDGEMENT)\n\n return (\n <Dialog open={open} onClose={() => !busy && onCancel()} maxWidth=\"sm\" fullWidth>\n <DialogTitle>\n {scope === 'org'\n ? `Uninstall ${name} across the organization?`\n : `Remove ${name} from this site?`}\n </DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n {scanning ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <CircularProgress size={16} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Checking what this would break…'}\n </Typography>\n </Stack>\n ) : null}\n\n {unknown && !scanning ? (\n <Alert severity=\"warning\">\n {pluginId\n ? 'The impact check could not run, so this cannot say what ' +\n 'would stop working. Nothing here means \"nothing breaks\".'\n : 'This listing records no plugin id, so its placements cannot ' +\n 'be found. Pages using it may stop rendering it.'}\n </Alert>\n ) : null}\n\n {!scanning && !unknown && affected > 0 ? (\n <Alert severity=\"error\">\n {`Removes ${name} from ${impact?.losingSites ?? losing.length} ` +\n `site${(impact?.losingSites ?? losing.length) === 1 ? '' : 's'}; ` +\n `${impact?.truncated ? 'at least ' : ''}${affected} published ` +\n `page${affected === 1 ? '' : 's'} ` +\n `place${affected === 1 ? 's' : ''} it and will stop ` +\n 'rendering it.'}\n </Alert>\n ) : null}\n\n {!scanning && !unknown && affected === 0 && losing.length ? (\n <Alert severity=\"info\">\n {'No published page places this plugin, so nothing visitors can ' +\n 'see changes.'}\n </Alert>\n ) : null}\n\n {covered.length ? (\n // The case that would otherwise read as a lie.\n <Alert severity=\"info\">\n {covered.length === targets.length\n ? 'The plugin keeps running on ' +\n (covered.length === 1\n ? 'this site'\n : `all ${covered.length} of these sites`) +\n ', because another install still covers ' +\n (covered.length === 1 ? 'it' : 'them') +\n '. Only which version is in use changes.'\n : `${covered.length} of these sites keep the plugin — ` +\n `${covered\n .map((target) => target.label)\n .join(', ')} — because another install still covers them.`}\n </Alert>\n ) : null}\n\n {impact?.sites\n .filter((site) => !site.stillCovered && site.placements.length)\n .map((site) => (\n <Box key={site.hostId}>\n <Typography variant=\"subtitle2\">{site.label}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${site.affectedScreens} published page` +\n `${site.affectedScreens === 1 ? '' : 's'} affected`}\n </Typography>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', gap: 1, marginTop: 1 }}\n >\n {site.placements.slice(0, 12).map((placement) => (\n <Chip\n key={`${placement.type}:${placement.id}`}\n size=\"small\"\n variant=\"outlined\"\n label={\n `${PLACEMENT_NOUNS[placement.type]}: ${placement.name}` +\n (placement.count > 1 ? ` ×${placement.count}` : '')\n }\n />\n ))}\n {site.placements.length > 12 ? (\n <Chip\n size=\"small\"\n label={`+${site.placements.length - 12} more`}\n />\n ) : null}\n </Stack>\n </Box>\n ))}\n\n {!scanning && losing.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Uninstalling removes the install only. The plugin’s settings ' +\n 'and any data it stored are kept, and installing it again ' +\n 'restores the pages that place it.'}\n </Typography>\n ) : null}\n\n {needsAcknowledgement && !scanning ? (\n <TextField\n size=\"small\"\n label={`Type ${ACKNOWLEDGEMENT} to confirm`}\n value={typed}\n onChange={(event) => setTyped(event.target.value)}\n autoComplete=\"off\"\n />\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button disabled={busy} onClick={onCancel}>\n {'Cancel'}\n </Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n disabled={!canConfirm}\n onClick={() => void confirm()}\n >\n {scope === 'org' ? 'Uninstall org-wide' : 'Remove'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nUninstallImpactDialog.displayName = 'UninstallImpactDialog'\n\nexport default UninstallImpactDialog\n"],"names":["useUser","authorizedFetch","Alert","Box","Button","Chip","CircularProgress","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","useCallback","useEffect","useState","ACKNOWLEDGEMENT","PLACEMENT_NOUNS","screen","layout","component","UninstallImpactDialog","props","open","listing","targets","scope","onCancel","onConfirm","data","user","impact","setImpact","failed","setFailed","typed","setTyped","busy","setBusy","pluginId","String","losing","filter","target","stillCovered","covered","targetKey","map","hostId","join","length","active","response","method","headers","body","JSON","stringify","hostIds","stillCoveredHostIds","payload","json","catch","ok","confirm","name","displayName","scanning","Boolean","affected","affectedScreens","unknown","needsAcknowledgement","canConfirm","trim","toUpperCase","onClose","maxWidth","fullWidth","dividers","spacing","direction","sx","alignItems","size","variant","color","severity","losingSites","truncated","label","sites","site","placements","useFlexGap","flexWrap","gap","marginTop","slice","placement","type","count","id","value","onChange","event","autoComplete","disabled","onClick"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/uninstall-impact-dialog.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { UninstallTarget } from '../model/marketplace'\nimport {\n UNINSTALL_ACKNOWLEDGEMENT,\n type UninstallImpact,\n uninstallImpactView,\n} from '../model/marketplace-catalog'\n\nexport interface UninstallImpactDialogProps {\n open: boolean\n listing: { $id?: string; displayName?: string; pluginId?: string } | null\n /** Sites this uninstall touches — see `resolveUninstallTargets`. */\n targets: readonly UninstallTarget[]\n scope: 'org' | 'host'\n onCancel: () => void\n onConfirm: () => void | Promise<unknown>\n}\n\n/**\n * What an uninstall breaks, before it happens (AGL-1027).\n *\n * Uninstalling was a single click with no confirmation and no statement of\n * consequence — the pin went away and every page placing the plugin stopped\n * rendering it, on live sites, silently. A generic \"are you sure?\" would not\n * have fixed that: a confirmation that only asks whether you are sure trains\n * people to click through it. This one names what stops working, and scales its\n * friction to the finding:\n *\n * * nothing placed anywhere → a plain confirm, because there is nothing to\n * weigh and adding ceremony to a harmless action is how ceremony stops\n * being read;\n * * published pages affected → a typed acknowledgement, because the number is\n * the point and typing it is the only way to be sure it was seen.\n *\n * It also has to say when uninstalling changes NOTHING visible: removing a host\n * pin while an org pin still covers the site swaps which pointer is in use, and\n * a dialog that implied the plugin was about to stop working would be wrong in\n * the direction that stops people trusting it.\n */\nexport function UninstallImpactDialog(props: UninstallImpactDialogProps) {\n const { open, listing, targets, scope, onCancel, onConfirm } = props\n const { data: user } = useUser()\n const [impact, setImpact] = useState<UninstallImpact | null>(null)\n const [failed, setFailed] = useState(false)\n const [typed, setTyped] = useState('')\n const [busy, setBusy] = useState(false)\n\n const pluginId = listing?.pluginId ? String(listing.pluginId) : ''\n const losing = targets.filter((target) => !target.stillCovered)\n const covered = targets.filter((target) => target.stillCovered)\n const targetKey = targets.map((target) => target.hostId).join(',')\n\n useEffect(() => {\n if (!open) {\n setImpact(null)\n setFailed(false)\n setTyped('')\n return\n }\n // Nothing to scan: either the plugin has no manifest id to match nodes on,\n // or every site keeps it. Both are answerable without a request.\n if (!pluginId || !losing.length) {\n setImpact(null)\n return\n }\n let active = true\n setFailed(false)\n void (async () => {\n try {\n const response = await authorizedFetch(\n user,\n '/api/hosts/plugin-impact',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n pluginId,\n hostIds: targets.map((target) => target.hostId),\n stillCoveredHostIds: covered.map((target) => target.hostId),\n }),\n },\n )\n const payload = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok || !payload) return void setFailed(true)\n setImpact(payload as UninstallImpact)\n } catch {\n if (active) setFailed(true)\n }\n })()\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, pluginId, targetKey, user])\n\n const confirm = useCallback(async () => {\n setBusy(true)\n try {\n await onConfirm()\n } finally {\n setBusy(false)\n }\n }, [onConfirm])\n\n /*\n * The scan could not speak, so it must not be read as \"nothing breaks\": a\n * failed check is a reason for MORE friction, not less — the one thing it\n * rules out is confidence. What the dialog says, and how much friction it\n * asks for, is `uninstallImpactView`, which the native apps replay.\n */\n const view = uninstallImpactView({\n name: listing?.displayName ?? 'this plugin',\n scope,\n pluginId,\n targets,\n impact,\n failed,\n })\n const canConfirm =\n !busy &&\n !view.scanning &&\n (!view.needsAcknowledgement ||\n typed.trim().toUpperCase() === UNINSTALL_ACKNOWLEDGEMENT)\n\n return (\n <Dialog open={open} onClose={() => !busy && onCancel()} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{view.title}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n {view.scanning ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <CircularProgress size={16} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Checking what this would break…'}\n </Typography>\n </Stack>\n ) : null}\n\n {view.alerts.map((alert) => (\n <Alert key={alert.text} severity={alert.tone}>\n {alert.text}\n </Alert>\n ))}\n\n {view.sites.map((site) => (\n <Box key={site.label}>\n <Typography variant=\"subtitle2\">{site.label}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {site.pages}\n </Typography>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', gap: 1, marginTop: 1 }}\n >\n {site.chips.map((chip) => (\n <Chip key={chip} size=\"small\" variant=\"outlined\" label={chip} />\n ))}\n </Stack>\n </Box>\n ))}\n\n {view.note ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {view.note}\n </Typography>\n ) : null}\n\n {view.needsAcknowledgement ? (\n <TextField\n size=\"small\"\n label={`Type ${UNINSTALL_ACKNOWLEDGEMENT} to confirm`}\n value={typed}\n onChange={(event) => setTyped(event.target.value)}\n autoComplete=\"off\"\n />\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button disabled={busy} onClick={onCancel}>\n {'Cancel'}\n </Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n disabled={!canConfirm}\n onClick={() => void confirm()}\n >\n {view.confirmLabel}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nUninstallImpactDialog.displayName = 'UninstallImpactDialog'\n\nexport default UninstallImpactDialog\n"],"names":["useUser","authorizedFetch","Alert","Box","Button","Chip","CircularProgress","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","useCallback","useEffect","useState","UNINSTALL_ACKNOWLEDGEMENT","uninstallImpactView","UninstallImpactDialog","props","open","listing","targets","scope","onCancel","onConfirm","data","user","impact","setImpact","failed","setFailed","typed","setTyped","busy","setBusy","pluginId","String","losing","filter","target","stillCovered","covered","targetKey","map","hostId","join","length","active","response","method","headers","body","JSON","stringify","hostIds","stillCoveredHostIds","payload","json","catch","ok","confirm","view","name","displayName","canConfirm","scanning","needsAcknowledgement","trim","toUpperCase","onClose","maxWidth","fullWidth","title","dividers","spacing","direction","sx","alignItems","size","variant","color","alerts","alert","severity","tone","text","sites","site","label","pages","useFlexGap","flexWrap","gap","marginTop","chips","chip","note","value","onChange","event","autoComplete","disabled","onClick","confirmLabel"],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@ import { useCallback, useRef } from "react";
|
|
|
23
23
|
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
24
24
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
25
25
|
import { listingArtifactType } from "../model/marketplace.js";
|
|
26
|
-
import { marketplaceInstallEndpoint, marketplaceLandingMessage } from "../model/marketplace-catalog.js";
|
|
26
|
+
import { installPlanOutcome, marketplaceInstallEndpoint, marketplaceLandingMessage } from "../model/marketplace-catalog.js";
|
|
27
27
|
/**
|
|
28
28
|
* What to tell someone whose install will never be three-way mergeable
|
|
29
29
|
* (AGL-2339).
|
|
@@ -269,27 +269,22 @@ import { marketplaceInstallEndpoint, marketplaceLandingMessage } from "../model/
|
|
|
269
269
|
}
|
|
270
270
|
return;
|
|
271
271
|
}
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
enqueueSnackbar((_errors_ = errors[0]) != null ? _errors_ : 'Install failed', {
|
|
289
|
-
variant: 'error',
|
|
290
|
-
allowDuplicate: true
|
|
291
|
-
});
|
|
292
|
-
}
|
|
272
|
+
const outcome = installPlanOutcome({
|
|
273
|
+
name: listing.displayName,
|
|
274
|
+
installed,
|
|
275
|
+
errors,
|
|
276
|
+
orgWide: steps.some((step)=>step.scope === 'org'),
|
|
277
|
+
updating: (options == null ? void 0 : options.intent) === 'update',
|
|
278
|
+
version: (options == null ? void 0 : options.version) != null ? String(options.version) : null,
|
|
279
|
+
saved: Boolean(marketplaceLandingMessage(artifactType, ''))
|
|
280
|
+
});
|
|
281
|
+
enqueueSnackbar(outcome.text, outcome.tone === 'success' ? {
|
|
282
|
+
variant: 'success',
|
|
283
|
+
persist: false
|
|
284
|
+
} : {
|
|
285
|
+
variant: outcome.tone,
|
|
286
|
+
allowDuplicate: true
|
|
287
|
+
});
|
|
293
288
|
for (const warning of abiWarnings){
|
|
294
289
|
enqueueSnackbar(warning, {
|
|
295
290
|
variant: 'warning',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-marketplace-actions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { readGaClientId } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { useLoading } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useCallback, useRef } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n type InstallPlanStep,\n listingArtifactType,\n} from '../model/marketplace'\nimport {\n marketplaceInstallEndpoint,\n marketplaceLandingMessage,\n} from '../model/marketplace-catalog'\n\n\n/**\n * What to tell someone whose install will never be three-way mergeable\n * (AGL-2339).\n *\n * Every install route returns `baseStored`, and `provenance.ts` says what a\n * false one means: *\"a snapshot that cannot be written leaves `baseStored:\n * false` and an artifact that reports itself as not updatable\"*. It reported\n * it to NOBODY — zero `.tsx` readers across all seven routes.\n *\n * The consequence is silent and expensive. `hasDivergedFromBase` needs the\n * base snapshot to tell the publisher's change from the user's; without one\n * an update can only overwrite. So a user who customizes a large artifact —\n * one whose content exceeded `ARTIFACT_BASE_MAX_BYTES`, or whose snapshot\n * write failed — LOSES those changes on the next update, having been told\n * nothing at install time. The update dialog does explain itself when the\n * moment arrives (`preview.mergeable` renders `preview.reason`), but by then\n * the choice is take-the-update-or-stay-behind, not \"keep a copy first\".\n *\n * Persistent rather than a timed toast: it is advice to act on before\n * editing, and it is the only moment this is knowable in advance.\n */\nfunction baseMissingWarning(displayName: string): string {\n return (\n `\"${displayName}\" is too large to keep a reference copy of, so a future ` +\n 'update will REPLACE it rather than merge. Export or note any changes ' +\n 'you make to it.'\n )\n}\n\n/**\n * Shared install/buy handlers for marketplace listings (AGL-95): used by the\n * browse grid and the listing detail page. Install copies the pinned\n * version snapshot server-side (/api/marketplace/install); buy opens a\n * Stripe checkout (/api/marketplace/checkout) when configured.\n */\nexport function useMarketplaceActions(hostId: string, orgId?: string | null) {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n\n /**\n * One purchase attempt per listing, minted lazily (AGL-1697).\n *\n * Per-listing rather than per-click: minting inside `buy` would defeat the\n * whole point, because two clicks would mint two keys and the server would\n * see two purchases. Held in a ref rather than state so a re-render cannot\n * rotate a key mid-flight.\n *\n * It retires when the page does. That is the honest boundary for a redirect\n * flow — the buyer leaves for Stripe and comes back to a fresh mount — and it\n * means this key covers the double-click and the retry, while the server's\n * already-purchased check covers the buyer who returns tomorrow.\n */\n const purchaseAttempts = useRef(new Map<string, string>())\n\n // `scope` is only meaningful for artifact types that HAVE an org-scoped\n // pin — see INSTALL_TARGETS. Passing it for a template would be ignored\n // server-side, so callers ask the model rather than guessing.\n const install = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n options?: {\n /**\n * Called instead of an error toast when the install route refuses\n * because the workspace's copy has been EDITED since it was installed\n * (AGL-1018's 409, AGL-1034).\n *\n * The refusal is correct — that install would have destroyed their\n * work — but \"review the update\" is only useful advice if something\n * offers the review. Handling it here rather than at each call site\n * means every surface that installs a copied artifact gets the\n * recovery, including ones written later.\n */\n onDiverged?: () => void\n },\n ) => {\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n ...(scope ? { scope } : {}),\n }),\n })\n const payload = await response.json()\n if (!response.ok) {\n // The copy has diverged: hand it to the review rather than telling\n // someone to review something they cannot reach from here.\n if (payload?.diverged && options?.onDiverged) {\n return void options.onDiverged()\n }\n // An installs feature lockdown (AGL-1510/1532): \"Install failed\"\n // reads as a broken listing — the very thing an incident response\n // needs people NOT to conclude while a suspect listing is under\n // investigation. The server says installs are paused; say that.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Install failed', {\n variant: response.status === 402 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n }\n // Types that land inert must not read as \"your site changed\".\n const landed = marketplaceLandingMessage(artifactType, listing.displayName)\n enqueueSnackbar(\n landed ??\n (payload.updated\n ? `Updated \"${listing.displayName}\" to v${payload.version}`\n : `Installed \"${listing.displayName}\" — find it under Your components`),\n { variant: 'success', persist: false },\n )\n // No base snapshot: this copy can never be three-way merged, and a\n // future update will overwrite whatever the user does to it. The\n // route has always said so on the wire; nothing listened (AGL-2339).\n //\n // `=== false` rather than a falsy test: a route that omitted the\n // field entirely has told us nothing, and warning on silence would\n // put a scary, unactionable message on every install from any\n // endpoint that had not been updated yet.\n if (payload.baseStored === false) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n /**\n * What was, and was not, checked about an email somebody else wrote.\n *\n * Said HERE because this is the moment the design enters the\n * workspace, and because the two facts are the ones a sender cannot\n * recover afterwards by looking at the design. `unreviewed` is the\n * ordinary case — email starters are auto-listed like every other\n * copied artifact — so the sentence has to be a statement rather than\n * an alarm, and it must never be silence: an install that said nothing\n * would be read as a review that happened.\n *\n * The link hosts are the inspectable half of the link policy. The\n * remote-asset rule is mechanical and the route enforces it, but where\n * a template points a tenant's own customers is a judgment only the\n * tenant can make, and they can only make it if they are shown the\n * list.\n */\n if (payload.assurance) {\n const hosts: string[] = payload.linkHosts ?? []\n enqueueSnackbar(\n (payload.assurance === 'approved'\n ? 'Reviewed by Aglyn. '\n : 'Nobody at Aglyn has reviewed this design. ') +\n (hosts.length\n ? `It links to ${hosts.join(', ')}. `\n : 'It links nowhere outside your site. ') +\n 'Read it before you send from it.',\n { variant: 'info', persist: true },\n )\n }\n // A media reference that still resolves against the publisher's own\n // library — served by our CDN, so no third party learns who opened the\n // mail, but the picture behind it is still theirs to change.\n if (payload.foreignMediaScopes?.length) {\n enqueueSnackbar(\n 'Some images in this email are still served from the publisher’s ' +\n 'own media library, so they can change after you install. ' +\n 'Replace them with your own to pin them.',\n { variant: 'info', persist: false },\n )\n }\n // A schema whose reference fields couldn't be relinked installs with\n // those fields degraded to text — silently changing a field's type\n // would be the kind of thing you discover much later (AGL-657).\n if (payload.degradedFieldIds?.length) {\n enqueueSnackbar(\n `${payload.degradedFieldIds.length} reference field(s) became ` +\n 'plain text — the datasets they pointed at are not in this ' +\n 'organization.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Fans an install-targeting plan out to the install routes (AGL-773). Each\n * step is either a single org pin or a host pin on a named site; org steps\n * resolve the org from the acting `hostId`. One summary snackbar reports the\n * whole plan rather than N toasts. Use `resolveInstallPlan` (model) to turn\n * an All-sites / Selected-sites choice into these steps.\n */\n const installPlan = useCallback(\n async (\n listing: any,\n steps: readonly InstallPlanStep[],\n options?: {\n /**\n * Re-pinning something already installed (AGL-1017). Only the wording\n * changes — the route is the same, because moving a pin forward IS\n * installing — but \"Installed on 1 site\" is the wrong sentence to read\n * after pressing Update.\n */\n intent?: 'install' | 'update'\n /** The version being moved to, for the update summary. */\n version?: string | number | null\n },\n ) => {\n if (!steps.length) return\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n let installed = 0\n const errors: string[] = []\n /**\n * Surfaced separately from success (AGL-1017/429): the pin is written\n * either way — it is an explicit choice — but a bundle built for a\n * different host ABI will not load, and a green \"Updated\" toast alone\n * would leave the workspace believing it did.\n */\n const abiWarnings = new Set<string>()\n /**\n * Sites whose copy landed with no base snapshot (AGL-2339). Counted\n * rather than toasted per step: the fan-out installs the SAME\n * artifact everywhere, so the answer is the same on every site and N\n * identical warnings would read as N different problems.\n */\n let withoutBase = 0\n /**\n * A feature lockdown refuses every step identically (AGL-1532), so\n * the fan-out stops at the first one and says \"installs are paused\"\n * ONCE. N identical refusals stacked into the error summary would\n * read as N broken sites — the opposite of what the lock is saying.\n */\n let lockedNotice: ReturnType<typeof parseLockdownRefusal> = null\n for (const step of steps) {\n // Org steps still need a host to resolve the org server-side, so\n // fall back to the acting host; host steps target their own site.\n const targetHostId = step.scope === 'host' ? step.hostId : hostId\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId,\n ...(step.scope === 'org' ? { scope: 'org' } : {}),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) {\n installed += 1\n if (payload?.hostAbiWarning) {\n abiWarnings.add(String(payload.hostAbiWarning))\n }\n if (payload?.baseStored === false) withoutBase += 1\n } else {\n lockedNotice = parseLockdownRefusal(response.status, payload)\n if (lockedNotice) break\n errors.push(payload?.error ?? 'Install failed')\n }\n }\n if (lockedNotice) {\n enqueueSnackbar(lockdownRefusalText(lockedNotice), {\n variant: 'warning',\n persist: true,\n })\n // Whatever landed before the lock DID land — saying so is the\n // difference between \"nothing happened\" and a half-applied plan\n // the workspace would otherwise discover much later.\n if (installed) {\n enqueueSnackbar(\n `${installed} site(s) were already updated before the pause.`,\n { variant: 'info', persist: false },\n )\n }\n return\n }\n const orgWide = steps.some((step) => step.scope === 'org')\n const updating = options?.intent === 'update'\n const noun = updating\n ? 'Updated'\n : marketplaceLandingMessage(artifactType, '')\n ? 'Saved'\n : 'Installed'\n const toVersion =\n updating && options?.version != null ? ` to v${options.version}` : ''\n if (installed && !errors.length) {\n enqueueSnackbar(\n orgWide\n ? `${noun} \"${listing.displayName}\"${toVersion} for the whole organization`\n : `${noun} \"${listing.displayName}\"${toVersion} on ${installed} site` +\n (installed === 1 ? '' : 's'),\n { variant: 'success', persist: false },\n )\n } else if (installed && errors.length) {\n enqueueSnackbar(\n `${noun} on ${installed} site(s); ${errors.length} failed — ${errors[0]}`,\n { variant: 'warning', allowDuplicate: true },\n )\n } else {\n enqueueSnackbar(errors[0] ?? 'Install failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n for (const warning of abiWarnings) {\n enqueueSnackbar(warning, { variant: 'warning', allowDuplicate: true })\n }\n if (withoutBase) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n const buy = useCallback(\n async (listing: any) => {\n const dequeue = queueLoading()\n try {\n const listingId = String(listing.$id)\n const attemptKey =\n purchaseAttempts.current.get(listingId) ??\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n purchaseAttempts.current.set(listingId, attemptKey)\n const response = await authorizedFetch(\n user,\n '/api/marketplace/checkout',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n // Stable across a retry of THIS attempt (AGL-1697), so a\n // double-click or a re-submit after a timeout cannot open a\n // second Stripe session on a live account.\n 'Idempotency-Key': attemptKey,\n },\n // The browser's GA client id rides along so the SERVER-side\n // `purchase` the Stripe webhook sends can be attributed to the\n // session — and therefore the campaign — that produced it\n // (AGL-1638). The webhook has always read this off the session\n // metadata; until now nothing captured it, so every marketplace\n // sale landed on a synthetic, sessionless GA user and plugin\n // revenue had no acquisition channel at all.\n //\n // Resolves to null within 500ms when gtag is absent (consent\n // refused, ad blocker, analytics unconfigured), so it can never\n // delay or block a purchase — same contract as the console's\n // subscription checkout.\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n // WHICH WORKSPACE IS BUYING (AGL-2331). A purchase licenses an\n // organization, so the acting org is named rather than inferred:\n // at org scope the surface acts through an arbitrary FIRST site,\n // and letting the server derive the licence holder from that site\n // would make which client workspace got the licence depend on the\n // order sites happen to come back in.\n //\n // Advisory, not authority — the server resolves it through the\n // caller's own membership and refuses an org they are not in. An\n // older cached bundle that sends none still works: the server\n // falls back to resolving the org from `hostId` the same way.\n ...(orgId ? { orgId } : {}),\n gaClientId: await readGaClientId(\n process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,\n ),\n }),\n },\n )\n const payload = await response.json()\n // The PAID install door refuses under a lock too (AGL-1545): both\n // `checkout` and `marketplace-installs` gate it. A buyer must not\n // read either lock as a payment failure — same reason the billing\n // page carries this branch (AGL-1532).\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n if (response.status === 501) {\n return void enqueueSnackbar(\n 'Purchases are not configured on this deployment',\n { variant: 'info', persist: false },\n )\n }\n if (!response.ok || !payload?.url) {\n return void enqueueSnackbar(payload?.error ?? 'Checkout failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n window.location.assign(payload.url)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, orgId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Uninstall a plugin listing from the listing detail (AGL-881) — the same\n * `action:'uninstall'` the installed add-ons card uses, so it removes the\n * pin (and the switchboard entry when nothing else pins it) and decrements\n * activeInstalls. Only plugins have a removable pin; other artifact types\n * land in a library and have nothing to uninstall.\n */\n const uninstall = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n // Which site's pin to drop (AGL-997). Without this the call always\n // targeted the acting host, so at org scope \"uninstall\" could only\n // mean \"the whole org pin\" or \"whichever site happened to be acting\"\n // — removing the plugin from ONE of several sites was unreachable.\n targetHostId?: string,\n ) => {\n const dequeue = queueLoading()\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/install-plugin',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId ?? hostId,\n action: 'uninstall',\n ...(scope ? { scope } : {}),\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // Uninstall rides the install-plugin route, so an installs lock\n // refuses it too (AGL-1532) — and \"Uninstall failed\" during an\n // incident is exactly the wrong sentence for someone trying to\n // remove the listing under investigation.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Uninstall failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(`Removed \"${listing.displayName}\"`, {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n return { install, installPlan, buy, uninstall }\n}\n\nexport default useMarketplaceActions\n"],"names":["lockdownRefusalText","parseLockdownRefusal","readGaClientId","useLoading","useSnackbar","useCallback","useRef","useUser","authorizedFetch","listingArtifactType","marketplaceInstallEndpoint","marketplaceLandingMessage","baseMissingWarning","displayName","useMarketplaceActions","hostId","orgId","data","user","enqueueSnackbar","queueLoading","purchaseAttempts","Map","install","listing","scope","options","dequeue","payload","artifactType","endpoint","response","method","headers","body","JSON","stringify","listingId","$id","json","ok","diverged","onDiverged","locked","status","variant","persist","error","allowDuplicate","landed","updated","version","baseStored","assurance","hosts","linkHosts","length","join","foreignMediaScopes","degradedFieldIds","console","installPlan","steps","installed","errors","abiWarnings","Set","withoutBase","lockedNotice","step","targetHostId","catch","hostAbiWarning","add","String","push","orgWide","some","updating","intent","noun","toVersion","warning","buy","attemptKey","current","get","Date","now","toString","Math","random","slice","set","gaClientId","process","env","NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID","url","window","location","assign","uninstall","action"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-marketplace-actions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { readGaClientId } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { useLoading } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useCallback, useRef } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n type InstallPlanStep,\n listingArtifactType,\n} from '../model/marketplace'\nimport {\n installPlanOutcome,\n marketplaceInstallEndpoint,\n marketplaceLandingMessage,\n} from '../model/marketplace-catalog'\n\n\n/**\n * What to tell someone whose install will never be three-way mergeable\n * (AGL-2339).\n *\n * Every install route returns `baseStored`, and `provenance.ts` says what a\n * false one means: *\"a snapshot that cannot be written leaves `baseStored:\n * false` and an artifact that reports itself as not updatable\"*. It reported\n * it to NOBODY — zero `.tsx` readers across all seven routes.\n *\n * The consequence is silent and expensive. `hasDivergedFromBase` needs the\n * base snapshot to tell the publisher's change from the user's; without one\n * an update can only overwrite. So a user who customizes a large artifact —\n * one whose content exceeded `ARTIFACT_BASE_MAX_BYTES`, or whose snapshot\n * write failed — LOSES those changes on the next update, having been told\n * nothing at install time. The update dialog does explain itself when the\n * moment arrives (`preview.mergeable` renders `preview.reason`), but by then\n * the choice is take-the-update-or-stay-behind, not \"keep a copy first\".\n *\n * Persistent rather than a timed toast: it is advice to act on before\n * editing, and it is the only moment this is knowable in advance.\n */\nfunction baseMissingWarning(displayName: string): string {\n return (\n `\"${displayName}\" is too large to keep a reference copy of, so a future ` +\n 'update will REPLACE it rather than merge. Export or note any changes ' +\n 'you make to it.'\n )\n}\n\n/**\n * Shared install/buy handlers for marketplace listings (AGL-95): used by the\n * browse grid and the listing detail page. Install copies the pinned\n * version snapshot server-side (/api/marketplace/install); buy opens a\n * Stripe checkout (/api/marketplace/checkout) when configured.\n */\nexport function useMarketplaceActions(hostId: string, orgId?: string | null) {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n\n /**\n * One purchase attempt per listing, minted lazily (AGL-1697).\n *\n * Per-listing rather than per-click: minting inside `buy` would defeat the\n * whole point, because two clicks would mint two keys and the server would\n * see two purchases. Held in a ref rather than state so a re-render cannot\n * rotate a key mid-flight.\n *\n * It retires when the page does. That is the honest boundary for a redirect\n * flow — the buyer leaves for Stripe and comes back to a fresh mount — and it\n * means this key covers the double-click and the retry, while the server's\n * already-purchased check covers the buyer who returns tomorrow.\n */\n const purchaseAttempts = useRef(new Map<string, string>())\n\n // `scope` is only meaningful for artifact types that HAVE an org-scoped\n // pin — see INSTALL_TARGETS. Passing it for a template would be ignored\n // server-side, so callers ask the model rather than guessing.\n const install = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n options?: {\n /**\n * Called instead of an error toast when the install route refuses\n * because the workspace's copy has been EDITED since it was installed\n * (AGL-1018's 409, AGL-1034).\n *\n * The refusal is correct — that install would have destroyed their\n * work — but \"review the update\" is only useful advice if something\n * offers the review. Handling it here rather than at each call site\n * means every surface that installs a copied artifact gets the\n * recovery, including ones written later.\n */\n onDiverged?: () => void\n },\n ) => {\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n ...(scope ? { scope } : {}),\n }),\n })\n const payload = await response.json()\n if (!response.ok) {\n // The copy has diverged: hand it to the review rather than telling\n // someone to review something they cannot reach from here.\n if (payload?.diverged && options?.onDiverged) {\n return void options.onDiverged()\n }\n // An installs feature lockdown (AGL-1510/1532): \"Install failed\"\n // reads as a broken listing — the very thing an incident response\n // needs people NOT to conclude while a suspect listing is under\n // investigation. The server says installs are paused; say that.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Install failed', {\n variant: response.status === 402 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n }\n // Types that land inert must not read as \"your site changed\".\n const landed = marketplaceLandingMessage(artifactType, listing.displayName)\n enqueueSnackbar(\n landed ??\n (payload.updated\n ? `Updated \"${listing.displayName}\" to v${payload.version}`\n : `Installed \"${listing.displayName}\" — find it under Your components`),\n { variant: 'success', persist: false },\n )\n // No base snapshot: this copy can never be three-way merged, and a\n // future update will overwrite whatever the user does to it. The\n // route has always said so on the wire; nothing listened (AGL-2339).\n //\n // `=== false` rather than a falsy test: a route that omitted the\n // field entirely has told us nothing, and warning on silence would\n // put a scary, unactionable message on every install from any\n // endpoint that had not been updated yet.\n if (payload.baseStored === false) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n /**\n * What was, and was not, checked about an email somebody else wrote.\n *\n * Said HERE because this is the moment the design enters the\n * workspace, and because the two facts are the ones a sender cannot\n * recover afterwards by looking at the design. `unreviewed` is the\n * ordinary case — email starters are auto-listed like every other\n * copied artifact — so the sentence has to be a statement rather than\n * an alarm, and it must never be silence: an install that said nothing\n * would be read as a review that happened.\n *\n * The link hosts are the inspectable half of the link policy. The\n * remote-asset rule is mechanical and the route enforces it, but where\n * a template points a tenant's own customers is a judgment only the\n * tenant can make, and they can only make it if they are shown the\n * list.\n */\n if (payload.assurance) {\n const hosts: string[] = payload.linkHosts ?? []\n enqueueSnackbar(\n (payload.assurance === 'approved'\n ? 'Reviewed by Aglyn. '\n : 'Nobody at Aglyn has reviewed this design. ') +\n (hosts.length\n ? `It links to ${hosts.join(', ')}. `\n : 'It links nowhere outside your site. ') +\n 'Read it before you send from it.',\n { variant: 'info', persist: true },\n )\n }\n // A media reference that still resolves against the publisher's own\n // library — served by our CDN, so no third party learns who opened the\n // mail, but the picture behind it is still theirs to change.\n if (payload.foreignMediaScopes?.length) {\n enqueueSnackbar(\n 'Some images in this email are still served from the publisher’s ' +\n 'own media library, so they can change after you install. ' +\n 'Replace them with your own to pin them.',\n { variant: 'info', persist: false },\n )\n }\n // A schema whose reference fields couldn't be relinked installs with\n // those fields degraded to text — silently changing a field's type\n // would be the kind of thing you discover much later (AGL-657).\n if (payload.degradedFieldIds?.length) {\n enqueueSnackbar(\n `${payload.degradedFieldIds.length} reference field(s) became ` +\n 'plain text — the datasets they pointed at are not in this ' +\n 'organization.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Fans an install-targeting plan out to the install routes (AGL-773). Each\n * step is either a single org pin or a host pin on a named site; org steps\n * resolve the org from the acting `hostId`. One summary snackbar reports the\n * whole plan rather than N toasts. Use `resolveInstallPlan` (model) to turn\n * an All-sites / Selected-sites choice into these steps.\n */\n const installPlan = useCallback(\n async (\n listing: any,\n steps: readonly InstallPlanStep[],\n options?: {\n /**\n * Re-pinning something already installed (AGL-1017). Only the wording\n * changes — the route is the same, because moving a pin forward IS\n * installing — but \"Installed on 1 site\" is the wrong sentence to read\n * after pressing Update.\n */\n intent?: 'install' | 'update'\n /** The version being moved to, for the update summary. */\n version?: string | number | null\n },\n ) => {\n if (!steps.length) return\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n let installed = 0\n const errors: string[] = []\n /**\n * Surfaced separately from success (AGL-1017/429): the pin is written\n * either way — it is an explicit choice — but a bundle built for a\n * different host ABI will not load, and a green \"Updated\" toast alone\n * would leave the workspace believing it did.\n */\n const abiWarnings = new Set<string>()\n /**\n * Sites whose copy landed with no base snapshot (AGL-2339). Counted\n * rather than toasted per step: the fan-out installs the SAME\n * artifact everywhere, so the answer is the same on every site and N\n * identical warnings would read as N different problems.\n */\n let withoutBase = 0\n /**\n * A feature lockdown refuses every step identically (AGL-1532), so\n * the fan-out stops at the first one and says \"installs are paused\"\n * ONCE. N identical refusals stacked into the error summary would\n * read as N broken sites — the opposite of what the lock is saying.\n */\n let lockedNotice: ReturnType<typeof parseLockdownRefusal> = null\n for (const step of steps) {\n // Org steps still need a host to resolve the org server-side, so\n // fall back to the acting host; host steps target their own site.\n const targetHostId = step.scope === 'host' ? step.hostId : hostId\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId,\n ...(step.scope === 'org' ? { scope: 'org' } : {}),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) {\n installed += 1\n if (payload?.hostAbiWarning) {\n abiWarnings.add(String(payload.hostAbiWarning))\n }\n if (payload?.baseStored === false) withoutBase += 1\n } else {\n lockedNotice = parseLockdownRefusal(response.status, payload)\n if (lockedNotice) break\n errors.push(payload?.error ?? 'Install failed')\n }\n }\n if (lockedNotice) {\n enqueueSnackbar(lockdownRefusalText(lockedNotice), {\n variant: 'warning',\n persist: true,\n })\n // Whatever landed before the lock DID land — saying so is the\n // difference between \"nothing happened\" and a half-applied plan\n // the workspace would otherwise discover much later.\n if (installed) {\n enqueueSnackbar(\n `${installed} site(s) were already updated before the pause.`,\n { variant: 'info', persist: false },\n )\n }\n return\n }\n const outcome = installPlanOutcome({\n name: listing.displayName,\n installed,\n errors,\n orgWide: steps.some((step) => step.scope === 'org'),\n updating: options?.intent === 'update',\n version: options?.version != null ? String(options.version) : null,\n saved: Boolean(marketplaceLandingMessage(artifactType, '')),\n })\n enqueueSnackbar(\n outcome.text,\n outcome.tone === 'success'\n ? { variant: 'success', persist: false }\n : { variant: outcome.tone, allowDuplicate: true },\n )\n for (const warning of abiWarnings) {\n enqueueSnackbar(warning, { variant: 'warning', allowDuplicate: true })\n }\n if (withoutBase) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n const buy = useCallback(\n async (listing: any) => {\n const dequeue = queueLoading()\n try {\n const listingId = String(listing.$id)\n const attemptKey =\n purchaseAttempts.current.get(listingId) ??\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n purchaseAttempts.current.set(listingId, attemptKey)\n const response = await authorizedFetch(\n user,\n '/api/marketplace/checkout',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n // Stable across a retry of THIS attempt (AGL-1697), so a\n // double-click or a re-submit after a timeout cannot open a\n // second Stripe session on a live account.\n 'Idempotency-Key': attemptKey,\n },\n // The browser's GA client id rides along so the SERVER-side\n // `purchase` the Stripe webhook sends can be attributed to the\n // session — and therefore the campaign — that produced it\n // (AGL-1638). The webhook has always read this off the session\n // metadata; until now nothing captured it, so every marketplace\n // sale landed on a synthetic, sessionless GA user and plugin\n // revenue had no acquisition channel at all.\n //\n // Resolves to null within 500ms when gtag is absent (consent\n // refused, ad blocker, analytics unconfigured), so it can never\n // delay or block a purchase — same contract as the console's\n // subscription checkout.\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n // WHICH WORKSPACE IS BUYING (AGL-2331). A purchase licenses an\n // organization, so the acting org is named rather than inferred:\n // at org scope the surface acts through an arbitrary FIRST site,\n // and letting the server derive the licence holder from that site\n // would make which client workspace got the licence depend on the\n // order sites happen to come back in.\n //\n // Advisory, not authority — the server resolves it through the\n // caller's own membership and refuses an org they are not in. An\n // older cached bundle that sends none still works: the server\n // falls back to resolving the org from `hostId` the same way.\n ...(orgId ? { orgId } : {}),\n gaClientId: await readGaClientId(\n process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,\n ),\n }),\n },\n )\n const payload = await response.json()\n // The PAID install door refuses under a lock too (AGL-1545): both\n // `checkout` and `marketplace-installs` gate it. A buyer must not\n // read either lock as a payment failure — same reason the billing\n // page carries this branch (AGL-1532).\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n if (response.status === 501) {\n return void enqueueSnackbar(\n 'Purchases are not configured on this deployment',\n { variant: 'info', persist: false },\n )\n }\n if (!response.ok || !payload?.url) {\n return void enqueueSnackbar(payload?.error ?? 'Checkout failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n window.location.assign(payload.url)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, orgId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Uninstall a plugin listing from the listing detail (AGL-881) — the same\n * `action:'uninstall'` the installed add-ons card uses, so it removes the\n * pin (and the switchboard entry when nothing else pins it) and decrements\n * activeInstalls. Only plugins have a removable pin; other artifact types\n * land in a library and have nothing to uninstall.\n */\n const uninstall = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n // Which site's pin to drop (AGL-997). Without this the call always\n // targeted the acting host, so at org scope \"uninstall\" could only\n // mean \"the whole org pin\" or \"whichever site happened to be acting\"\n // — removing the plugin from ONE of several sites was unreachable.\n targetHostId?: string,\n ) => {\n const dequeue = queueLoading()\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/install-plugin',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId ?? hostId,\n action: 'uninstall',\n ...(scope ? { scope } : {}),\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // Uninstall rides the install-plugin route, so an installs lock\n // refuses it too (AGL-1532) — and \"Uninstall failed\" during an\n // incident is exactly the wrong sentence for someone trying to\n // remove the listing under investigation.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Uninstall failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(`Removed \"${listing.displayName}\"`, {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n return { install, installPlan, buy, uninstall }\n}\n\nexport default useMarketplaceActions\n"],"names":["lockdownRefusalText","parseLockdownRefusal","readGaClientId","useLoading","useSnackbar","useCallback","useRef","useUser","authorizedFetch","listingArtifactType","installPlanOutcome","marketplaceInstallEndpoint","marketplaceLandingMessage","baseMissingWarning","displayName","useMarketplaceActions","hostId","orgId","data","user","enqueueSnackbar","queueLoading","purchaseAttempts","Map","install","listing","scope","options","dequeue","payload","artifactType","endpoint","response","method","headers","body","JSON","stringify","listingId","$id","json","ok","diverged","onDiverged","locked","status","variant","persist","error","allowDuplicate","landed","updated","version","baseStored","assurance","hosts","linkHosts","length","join","foreignMediaScopes","degradedFieldIds","console","installPlan","steps","installed","errors","abiWarnings","Set","withoutBase","lockedNotice","step","targetHostId","catch","hostAbiWarning","add","String","push","outcome","name","orgWide","some","updating","intent","saved","Boolean","text","tone","warning","buy","attemptKey","current","get","Date","now","toString","Math","random","slice","set","gaClientId","process","env","NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID","url","window","location","assign","uninstall","action"],"mappings":"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"}
|
|
@@ -94,3 +94,291 @@ export declare function marketplaceInstallEndpoint(artifactType: string): string
|
|
|
94
94
|
* version the owner still has to activate.
|
|
95
95
|
*/
|
|
96
96
|
export declare function marketplaceLandingMessage(artifactType: string, displayName: string): string | null;
|
|
97
|
+
/**
|
|
98
|
+
* ─── WHAT A PLUGIN'S PAGE SAYS ABOUT ITS INSTALLS (AGL-1016, AGL-997, AGL-1027, AGL-3671) ───
|
|
99
|
+
*
|
|
100
|
+
* The version comparison, the update status, the org-wide picture of one listing's pins, which sites an
|
|
101
|
+
* uninstall touches and the sentences the workspace's plugin pages and their dialogs say about them. All of
|
|
102
|
+
* it was spread across `update-state.ts`, `marketplace.ts` and the plugin-site-set components; it lives here,
|
|
103
|
+
* pure, so the native apps replay the very same answers (`libs/native/contracts/function-cases.generated.json`).
|
|
104
|
+
* Those modules re-export what moved.
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
107
|
+
* Orders two artifact versions, or returns null when they cannot be ordered.
|
|
108
|
+
*
|
|
109
|
+
* Two numbering schemes are in play and both are real: copied artifacts get a
|
|
110
|
+
* monotonic integer from the publish route, plugins get the semver string off
|
|
111
|
+
* their manifest. A single string compare handles neither ("10" < "9",
|
|
112
|
+
* "1.10.0" < "1.9.0"), so both are parsed as dotted numbers — which covers the
|
|
113
|
+
* integer case as a one-segment version.
|
|
114
|
+
*
|
|
115
|
+
* Anything non-numeric (a prerelease tag, a date-stamped build) is not ordered
|
|
116
|
+
* by guesswork: unequal-and-incomparable is reported as such and surfaces as
|
|
117
|
+
* `unknown`, which is honest, where a string compare would confidently point
|
|
118
|
+
* an update button at an older build.
|
|
119
|
+
*/
|
|
120
|
+
export declare function compareArtifactVersions(a: string | number | null | undefined, b: string | number | null | undefined): -1 | 0 | 1 | null;
|
|
121
|
+
export type UpdateState =
|
|
122
|
+
/** Running the newest version the publisher offers. */
|
|
123
|
+
'current'
|
|
124
|
+
/** A newer version exists and can be taken. */
|
|
125
|
+
| 'update-available'
|
|
126
|
+
/**
|
|
127
|
+
* Installed version is NEWER than what is offered. Not a bug: a publisher
|
|
128
|
+
* installs their own unapproved version to test it, and a version can be
|
|
129
|
+
* withdrawn after someone installed it.
|
|
130
|
+
*/
|
|
131
|
+
| 'ahead'
|
|
132
|
+
/** Not enough is known to say — no provenance, or nothing published to compare. */
|
|
133
|
+
| 'unknown';
|
|
134
|
+
export interface UpdateStatus {
|
|
135
|
+
state: UpdateState;
|
|
136
|
+
/** The version running, or null when provenance cannot say. */
|
|
137
|
+
installedVersion: string | null;
|
|
138
|
+
/** The newest version on offer — for plugins, the newest APPROVED one. */
|
|
139
|
+
availableVersion: string | null;
|
|
140
|
+
/**
|
|
141
|
+
* Why the answer is `unknown`, for a UI that should say which kind of
|
|
142
|
+
* silence this is rather than implying the artifact is current.
|
|
143
|
+
*/
|
|
144
|
+
unknownReason?: 'no-provenance' | 'nothing-published' | 'incomparable';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* One sentence describing an update state, so four surfaces do not each invent
|
|
148
|
+
* their own wording for the same fact.
|
|
149
|
+
*/
|
|
150
|
+
export declare function updateStateLabel(status: UpdateStatus): string;
|
|
151
|
+
/** The pin fields an update comparison reads, in every shape the install routes have written. */
|
|
152
|
+
export interface UpdateComparablePin {
|
|
153
|
+
version?: unknown;
|
|
154
|
+
installedFrom?: {
|
|
155
|
+
listingId?: string;
|
|
156
|
+
version?: unknown;
|
|
157
|
+
} | null;
|
|
158
|
+
marketplace?: {
|
|
159
|
+
listingId?: string;
|
|
160
|
+
version?: unknown;
|
|
161
|
+
} | null;
|
|
162
|
+
source?: {
|
|
163
|
+
listingId?: string;
|
|
164
|
+
version?: unknown;
|
|
165
|
+
} | null;
|
|
166
|
+
listingId?: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* What a listing OFFERS right now (the plugin branch of `offeredPluginVersion`): the newest approved version, unless a
|
|
170
|
+
* revocation (`versions` naming it, or `'all'`) has stopped it, in which case nothing is offered.
|
|
171
|
+
*/
|
|
172
|
+
export declare function pluginOfferedVersion(latestApprovedVersion: unknown, revocationVersions: readonly string[] | 'all' | null | undefined): string | null;
|
|
173
|
+
/**
|
|
174
|
+
* Whether the installed plugin is still what the publisher ships (the plugin branch of `resolveUpdateState`): the
|
|
175
|
+
* pin's version against the version on offer. A pin that names no listing is the one shape that records no version
|
|
176
|
+
* (`no-provenance`).
|
|
177
|
+
*/
|
|
178
|
+
export declare function pluginUpdateStatus(pin: UpdateComparablePin | null | undefined, offeredVersion: string | null | undefined): UpdateStatus;
|
|
179
|
+
/** One site's slice of an org-scope install picture (AGL-997). */
|
|
180
|
+
export interface OrgInstallSite {
|
|
181
|
+
hostId: string;
|
|
182
|
+
label: string;
|
|
183
|
+
/** The version that actually runs here, from the effective pin. */
|
|
184
|
+
version: string | null;
|
|
185
|
+
/** Where that pin lives — a host pin shadows the org one, as the loader does. */
|
|
186
|
+
pinnedBy: InstallTarget;
|
|
187
|
+
/** This site has its OWN pin on top of an org pin. */
|
|
188
|
+
shadowed: boolean;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The whole org's install picture for one listing (AGL-997).
|
|
192
|
+
*
|
|
193
|
+
* At org scope, "installed" is a SET, not a boolean. The detail page used to
|
|
194
|
+
* resolve state against the single acting host, so a plugin installed on one
|
|
195
|
+
* of five sites reported "Installed on this site" — describing one arbitrary
|
|
196
|
+
* site and staying silent about the other four — and offered an Uninstall
|
|
197
|
+
* that was all-or-nothing.
|
|
198
|
+
*
|
|
199
|
+
* `orgWide` and per-site pins are not exclusive: an org pin covers every
|
|
200
|
+
* site including ones created later, and a host pin on top of it shadows it
|
|
201
|
+
* for that one site. Both facts have to survive into the UI, which is why
|
|
202
|
+
* this returns the sites AND the pins rather than a single scope.
|
|
203
|
+
*/
|
|
204
|
+
export interface OrgInstallSummary {
|
|
205
|
+
/** An org pin exists: every site is covered, including future ones. */
|
|
206
|
+
orgWide: boolean;
|
|
207
|
+
/** Version of the org pin, when there is one. */
|
|
208
|
+
orgVersion: string | null;
|
|
209
|
+
/** Every site the listing effectively runs on. */
|
|
210
|
+
sites: OrgInstallSite[];
|
|
211
|
+
/** Sites carrying their own removable host pin. */
|
|
212
|
+
hostPinnedIds: string[];
|
|
213
|
+
/** Sites with no pin of their own and no org pin covering them. */
|
|
214
|
+
availableHostIds: string[];
|
|
215
|
+
installedAnywhere: boolean;
|
|
216
|
+
}
|
|
217
|
+
export declare function resolveOrgInstallSummary(hosts: ReadonlyArray<{
|
|
218
|
+
id: string;
|
|
219
|
+
label: string;
|
|
220
|
+
}>, hostPins: Readonly<Record<string, InstallPin | null | undefined>>, orgPin: InstallPin | null | undefined): OrgInstallSummary;
|
|
221
|
+
/** One site an uninstall would touch, and whether it really loses the plugin. */
|
|
222
|
+
export interface UninstallTarget {
|
|
223
|
+
hostId: string;
|
|
224
|
+
label: string;
|
|
225
|
+
/**
|
|
226
|
+
* The plugin keeps running here. Removing a HOST pin while an org pin still
|
|
227
|
+
* covers the site, or the ORG pin while the site holds its own host pin,
|
|
228
|
+
* changes which pointer is in use and nothing a visitor can see.
|
|
229
|
+
*/
|
|
230
|
+
stillCovered: boolean;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Which sites an uninstall actually affects (AGL-1027).
|
|
234
|
+
*
|
|
235
|
+
* The shadowing case is the whole reason this is a function rather than a
|
|
236
|
+
* filter at the call site. A pin is not a boolean per site: an org pin covers
|
|
237
|
+
* everything, a host pin names one site, and where both exist the host pin
|
|
238
|
+
* wins. So "remove" means three different things depending on which pointer is
|
|
239
|
+
* being dropped and what is left underneath — and the one thing the dialog must
|
|
240
|
+
* never do is tell someone a plugin will stop working when it will not.
|
|
241
|
+
*
|
|
242
|
+
* Returns every site the operation touches, including the ones that keep the
|
|
243
|
+
* plugin, so the dialog can say so explicitly instead of omitting them and
|
|
244
|
+
* leaving the count unexplained.
|
|
245
|
+
*/
|
|
246
|
+
export declare function resolveUninstallTargets(orgInstall: OrgInstallSummary, scope: 'org' | 'host', hostId?: string): UninstallTarget[];
|
|
247
|
+
/** The sites of an org-wide picture whose running version is behind the one on offer. */
|
|
248
|
+
export declare function staleSites(sites: readonly OrgInstallSite[], latestVersion: string | number | null | undefined): OrgInstallSite[];
|
|
249
|
+
/** What "Where it runs" opens with: the whole organization, or how many of its sites. */
|
|
250
|
+
export declare function pluginSiteSetSummary(install: OrgInstallSummary, siteCount: number): string;
|
|
251
|
+
/** What the update confirmation says: the plugin, the version it moves to and where, and what stays put. */
|
|
252
|
+
export declare function pluginUpdatePrompt(input: {
|
|
253
|
+
name: string;
|
|
254
|
+
latestVersion: string;
|
|
255
|
+
label: string;
|
|
256
|
+
/** The update is for one site: other sites stay on the version they run. */
|
|
257
|
+
singleSite: boolean;
|
|
258
|
+
}): {
|
|
259
|
+
title: string;
|
|
260
|
+
body: string;
|
|
261
|
+
others: string | null;
|
|
262
|
+
note: string;
|
|
263
|
+
};
|
|
264
|
+
/** What the update button of a pin that is org-wide, or of the stale per-site ones, is called and aims at. */
|
|
265
|
+
export declare function pluginUpdateAll(install: OrgInstallSummary, stale: readonly OrgInstallSite[], latestVersion: string): {
|
|
266
|
+
label: string;
|
|
267
|
+
target: string;
|
|
268
|
+
hostIds: string[];
|
|
269
|
+
};
|
|
270
|
+
/** The notice above an org-wide update: one pin moves every site at once. */
|
|
271
|
+
export declare const PLUGIN_ORG_WIDE_UPDATE_NOTE = "This is one organization-wide pin, so updating moves every site at once. To take a version on one site first, install per site instead.";
|
|
272
|
+
/** One site an uninstall dialog was told about by the impact check. */
|
|
273
|
+
export interface UninstallImpactSite {
|
|
274
|
+
hostId: string;
|
|
275
|
+
label: string;
|
|
276
|
+
stillCovered: boolean;
|
|
277
|
+
placements: Array<{
|
|
278
|
+
type: 'screen' | 'layout' | 'component';
|
|
279
|
+
id: string;
|
|
280
|
+
name: string;
|
|
281
|
+
count: number;
|
|
282
|
+
}>;
|
|
283
|
+
affectedScreens: number;
|
|
284
|
+
truncated: boolean;
|
|
285
|
+
}
|
|
286
|
+
/** What `/api/hosts/plugin-impact` answers. */
|
|
287
|
+
export interface UninstallImpact {
|
|
288
|
+
sites: UninstallImpactSite[];
|
|
289
|
+
losingSites: number;
|
|
290
|
+
placements: number;
|
|
291
|
+
affectedScreens: number;
|
|
292
|
+
truncated: boolean;
|
|
293
|
+
dataSurvives: boolean;
|
|
294
|
+
}
|
|
295
|
+
/** The word typed to confirm an uninstall that breaks pages, or whose impact could not be checked. */
|
|
296
|
+
export declare const UNINSTALL_ACKNOWLEDGEMENT = "UNINSTALL";
|
|
297
|
+
/** What the uninstall dialog shows, worked out from the targets and the impact check's answer. */
|
|
298
|
+
export interface UninstallImpactView {
|
|
299
|
+
title: string;
|
|
300
|
+
confirmLabel: string;
|
|
301
|
+
/** The check is still running. */
|
|
302
|
+
scanning: boolean;
|
|
303
|
+
/** Alerts above the sites, in order. */
|
|
304
|
+
alerts: Array<{
|
|
305
|
+
tone: 'warning' | 'error' | 'info';
|
|
306
|
+
text: string;
|
|
307
|
+
}>;
|
|
308
|
+
/** Sites that lose the plugin and place it on pages: the site, how many pages, and the chips. */
|
|
309
|
+
sites: Array<{
|
|
310
|
+
label: string;
|
|
311
|
+
pages: string;
|
|
312
|
+
chips: string[];
|
|
313
|
+
}>;
|
|
314
|
+
/** The closing note, when sites lose the plugin. */
|
|
315
|
+
note: string | null;
|
|
316
|
+
/** Pages stop rendering it, or the check could not say: the person types {@link UNINSTALL_ACKNOWLEDGEMENT}. */
|
|
317
|
+
needsAcknowledgement: boolean;
|
|
318
|
+
}
|
|
319
|
+
/** The uninstall dialog (`UninstallImpactDialog`) as data, so every client says the same things. */
|
|
320
|
+
export declare function uninstallImpactView(input: {
|
|
321
|
+
name: string;
|
|
322
|
+
scope: 'org' | 'host';
|
|
323
|
+
/** The listing's plugin id; without one the placements cannot be found. */
|
|
324
|
+
pluginId: string;
|
|
325
|
+
targets: readonly UninstallTarget[];
|
|
326
|
+
/** The impact check's answer, once it has come. */
|
|
327
|
+
impact: UninstallImpact | null;
|
|
328
|
+
/** The impact check failed. */
|
|
329
|
+
failed: boolean;
|
|
330
|
+
}): UninstallImpactView;
|
|
331
|
+
/** What a run of installs (an install, an update, or the several steps of one) says once it is done. */
|
|
332
|
+
export declare function installPlanOutcome(input: {
|
|
333
|
+
name: string;
|
|
334
|
+
/** Steps that went through, and the refusals of those that did not. */
|
|
335
|
+
installed: number;
|
|
336
|
+
errors: readonly string[];
|
|
337
|
+
/** One of the steps was the org-wide pin. */
|
|
338
|
+
orgWide: boolean;
|
|
339
|
+
/** The steps moved existing pins to {@link version}. */
|
|
340
|
+
updating: boolean;
|
|
341
|
+
version?: string | null;
|
|
342
|
+
/** The artifact lands in a library and is not live (a template, a layout): "Saved", not "Installed". */
|
|
343
|
+
saved?: boolean;
|
|
344
|
+
}): {
|
|
345
|
+
tone: 'success' | 'warning' | 'error';
|
|
346
|
+
text: string;
|
|
347
|
+
};
|
|
348
|
+
/** The row of the plugins list for one install: the version and where it runs. */
|
|
349
|
+
export declare function installedPluginCaption(input: {
|
|
350
|
+
version?: unknown;
|
|
351
|
+
orgWide: boolean;
|
|
352
|
+
siteLabels: readonly string[];
|
|
353
|
+
}): string;
|
|
354
|
+
interface ManifestConfigField {
|
|
355
|
+
key: string;
|
|
356
|
+
label: string;
|
|
357
|
+
type: 'string' | 'number' | 'boolean' | 'select';
|
|
358
|
+
description?: string;
|
|
359
|
+
options?: Array<{
|
|
360
|
+
value: string;
|
|
361
|
+
label: string;
|
|
362
|
+
}>;
|
|
363
|
+
min?: number;
|
|
364
|
+
max?: number;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The fields a plugin's MANIFEST declares for its settings and the values they start from (`pluginConfigSchemaFromManifest`
|
|
368
|
+
* of `plugin-config.ts`, which this repeats without its imports so the native apps replay it). Everything is re-validated
|
|
369
|
+
* rather than trusted: a junk type drops the field, a duplicate key drops the second, a select with nothing to select is
|
|
370
|
+
* dropped, and the count is capped. `null` when the manifest declares nothing usable, which means "this plugin has no
|
|
371
|
+
* settings".
|
|
372
|
+
*/
|
|
373
|
+
export declare function manifestSettingsFields(manifest: unknown): {
|
|
374
|
+
fields: ManifestConfigField[];
|
|
375
|
+
defaults: Record<string, unknown>;
|
|
376
|
+
} | null;
|
|
377
|
+
/**
|
|
378
|
+
* The workspace-settings screen of a marketplace plugin, as a native screen spec: the console's `PluginConfigCards` for a
|
|
379
|
+
* pin's manifest. The same shape the generated screens of the built-in plugins have (`pluginSettingsScreen` of
|
|
380
|
+
* `native-plugin-settings.mjs`), under an id of its own, saved to `orgs/{org}/pluginSettings/{plugin}` as the console
|
|
381
|
+
* saves it. `null` for a plugin that declares no settings.
|
|
382
|
+
*/
|
|
383
|
+
export declare function manifestSettingsScreen(pluginId: string, label: string, manifest: unknown): Record<string, unknown> | null;
|
|
384
|
+
export {};
|