@aglyn/plugins-marketplace 1.0.0-beta.254 → 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-marketplace",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.255",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.255",
|
|
29
|
+
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.255",
|
|
30
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.255",
|
|
31
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.255",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.255",
|
|
33
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.255",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.255",
|
|
35
|
+
"@aglyn/shared-util-email": "1.0.0-beta.255",
|
|
36
|
+
"@aglyn/shared-util-http": "1.0.0-beta.255",
|
|
37
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.255",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.255",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.255",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.255",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -23,6 +23,7 @@ import { Alert, Chip, Stack, Tooltip, Typography } from "@mui/material";
|
|
|
23
23
|
import { collection, documentId, getDocs, limit, query, where } from "firebase/firestore";
|
|
24
24
|
import { useEffect, useMemo, useState } from "react";
|
|
25
25
|
import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
26
|
+
import { installedPluginCaption } from "../model/marketplace-catalog.js";
|
|
26
27
|
import { resolveUpdateState, updateStateLabel } from "../model/update-state.js";
|
|
27
28
|
/**
|
|
28
29
|
* "Installed from the marketplace", on the workspace's Plugins page — the
|
|
@@ -172,7 +173,7 @@ import { resolveUpdateState, updateStateLabel } from "../model/update-state.js";
|
|
|
172
173
|
pluginRef: install.$id
|
|
173
174
|
}),
|
|
174
175
|
label: (_ref = (_install_displayName = install.displayName) != null ? _install_displayName : install.pluginId) != null ? _ref : install.$id,
|
|
175
|
-
caption:
|
|
176
|
+
caption: installedPluginCaption(install),
|
|
176
177
|
trailing: status.state === 'update-available' ? /*#__PURE__*/ _jsx(Chip, {
|
|
177
178
|
size: "small",
|
|
178
179
|
color: "primary",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/org-plugin-installs-card.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 {\n buildRoute,\n pluginDocsHelp,\n Route,\n type ConsoleOrgPluginInstallsZoneProps,\n} from '@aglyn/aglyn'\nimport { mdiChevronRight } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Alert, Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n documentId,\n getDocs,\n limit,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState, type ReactNode } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { resolveUpdateState, updateStateLabel } from '../model/update-state'\n\n/** One installation: its org pin, or the sites that pin it. */\ninterface Installation {\n $id: string\n displayName?: string\n pluginId?: string\n version?: string\n siteLabels: string[]\n orgWide: boolean\n}\n\n/**\n * \"Installed from the marketplace\", on the workspace's Plugins page — the\n * `orgPluginInstalls` zone (AGL-1011, drawn here since AGL-3080).\n *\n * The page is the workspace's inventory, first-party and marketplace\n * together, because where a plugin came from is a fact about it rather than a\n * different kind of thing. The built-in half is the shell's. This half is the\n * marketplace's: the pins are its `installs`, and the update chip compares\n * them with its listings and its kill switches — so the page hands over the\n * sites it can see and this card draws the rest.\n *\n * One row per INSTALLATION, not per pin: a plugin on three sites is one thing\n * an admin manages, and the scope is a fact about it rather than three\n * separate entries. An org pin covers every site, so it wins the caption\n * outright. Every row goes to the shell's installation page, which is where\n * an installation is managed.\n */\nexport function OrgPluginInstallsCard(props: ConsoleOrgPluginInstallsZoneProps) {\n const { orgId, orgSlug, hosts } = props\n const firestore = useFirestore()\n\n // Held at null while the org resolves, never `orgs/-pending-` (AGL-1440):\n // installs are member-gated, so the sentinel was a guaranteed-denied listen\n // on every mount.\n const { data: orgInstalls } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(collection(firestore, 'orgs', orgId, 'installs'), limit(100))\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n\n // Site pins too (AGL-1012). Reading only the org pins meant a plugin\n // targeted at specific sites — installed, running, and the whole point of\n // AGL-773/997 — was absent from the workspace's own plugin inventory.\n // Fan-in per site: the host count is data, so a hook per host would change\n // the hook count between renders.\n const hostIdsKey = hosts.map((host) => host.id).join('|')\n const [sitePins, setSitePins] = useState<Record<string, any[]>>({})\n useEffect(() => {\n if (!hosts.length) return\n let active = true\n void Promise.all(\n hosts.map(async (host) => {\n const snapshot = await getDocs(\n query(collection(firestore, 'hosts', host.id, 'installs'), limit(100)),\n )\n return [\n host.id,\n snapshot.docs.map((entry) => ({ $id: entry.id, ...entry.data() })),\n ] as const\n }),\n )\n .then((entries) => {\n if (active) setSitePins(Object.fromEntries(entries))\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, hostIdsKey])\n\n const installations = useMemo(() => {\n const byListing = new Map<string, Installation>()\n for (const install of orgInstalls ?? []) {\n byListing.set(install.$id, { ...install, siteLabels: [], orgWide: true })\n }\n for (const host of hosts) {\n for (const pin of sitePins[host.id] ?? []) {\n const existing = byListing.get(pin.$id)\n if (existing) existing.siteLabels.push(host.label)\n else\n byListing.set(pin.$id, {\n ...pin,\n siteLabels: [host.label],\n orgWide: false,\n })\n }\n }\n return [...byListing.values()]\n }, [orgInstalls, hosts, sitePins])\n\n /**\n * The listings behind those installs, for the update chip (AGL-1016), and\n * their kill switches (AGL-2368) — a revoked version stays `approved`,\n * because revocation does not clear a review verdict, so without them the\n * chip offered an update `install-plugin` answers 409 to.\n *\n * Both are public reads, so plain fetches — but `in` caps at 30 ids, and a\n * workspace can run more plugins than that, so they chunk. A failure leaves\n * a map empty and every row resolves to `unknown`, which reads as \"we can't\n * say\" rather than a false \"up to date\".\n */\n const listingIdsKey = installations\n .map((install) => install.$id)\n .sort()\n .join('|')\n const [listings, setListings] = useState<Record<string, any>>({})\n const [revocations, setRevocations] = useState<Record<string, any>>({})\n useEffect(() => {\n const ids = listingIdsKey ? listingIdsKey.split('|') : []\n if (!ids.length) return\n let active = true\n const chunks: string[][] = []\n for (let index = 0; index < ids.length; index += 30) {\n chunks.push(ids.slice(index, index + 30))\n }\n const fetchChunks = (path: string) =>\n Promise.all(\n chunks.map((chunk) =>\n getDocs(\n query(collection(firestore, path), where(documentId(), 'in', chunk)),\n ),\n ),\n ).then((results) =>\n Object.fromEntries(\n results.flatMap((snapshot) =>\n snapshot.docs.map((entry) => [entry.id, entry.data()]),\n ),\n ),\n )\n void fetchChunks('marketplaceListings')\n .then((map) => {\n if (active) setListings(map)\n })\n .catch(() => undefined)\n void fetchChunks('revocations')\n .then((map) => {\n if (active) setRevocations(map)\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, listingIdsKey])\n\n return (\n <CardDisplay\n header={'Installed from the marketplace'}\n // Card-level help, not just the page's (AGL-2129). This is the card\n // people arrive at looking for a switch, having read in Marketplace\n // that installing is done there — so it points at the step of the\n // walkthrough that explains the split.\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#installed-from-the-marketplace',\n })}\n contentGutterX\n contentGutterY\n >\n {installations.length ? (\n <Stack>\n {installations.map((install) => {\n // The update signal (AGL-1016/2368), from the one shared\n // comparison — for plugins that is the newest INSTALLABLE\n // version, so this can never offer what install refuses.\n const status = resolveUpdateState(\n install as never,\n listings[install.$id] ?? null,\n 'plugin',\n revocations[install.$id] ?? null,\n )\n return (\n <InstallationRow\n key={install.$id}\n href={buildRoute(Route.ORG_PLUGIN_INSTALLATION, {\n orgSlug,\n pluginRef: install.$id,\n })}\n label={install.displayName ?? install.pluginId ?? install.$id}\n caption={\n `v${install.version} · ` +\n (install.orgWide\n ? 'every site in this organization'\n : install.siteLabels.length === 1\n ? install.siteLabels[0]\n : `${install.siteLabels.length} sites`)\n }\n trailing={\n status.state === 'update-available' ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n label={`v${status.availableVersion} available`}\n />\n ) : status.state === 'unknown' ? (\n <Tooltip title={updateStateLabel(status)}>\n <Chip size=\"small\" variant=\"outlined\" label={'Unknown'} />\n </Tooltip>\n ) : undefined\n }\n />\n )\n })}\n </Stack>\n ) : (\n <Alert severity=\"info\">\n {'Nothing installed from the marketplace yet — browse it to add one.'}\n </Alert>\n )}\n </CardDisplay>\n )\n}\n\n/**\n * One row, drawn the way the page draws its built-in rows: the whole row is\n * the link and a chevron says so, because the entry point AGL-1011 replaced\n * was a name rendered as body text that nothing said went anywhere.\n */\nfunction InstallationRow(props: {\n href: string\n label: string\n caption: string\n trailing?: ReactNode\n}) {\n const { href, label, caption, trailing } = props\n return (\n <AppLink href={href} color=\"inherit\" underline=\"none\">\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n py: 1.25,\n px: 1,\n mx: -1,\n borderRadius: 1,\n '&:hover': { bgcolor: 'action.hover' },\n }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {caption}\n </Typography>\n </Stack>\n {trailing}\n <MdiIcon path={mdiChevronRight.path} color=\"disabled\" sx={{ fontSize: 20 }} />\n </Stack>\n </AppLink>\n )\n}\n\nexport default OrgPluginInstallsCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiChevronRight","AppLink","CardDisplay","MdiIcon","Alert","Chip","Stack","Tooltip","Typography","collection","documentId","getDocs","limit","query","where","useEffect","useMemo","useState","useFirestore","useFirestoreCollection","resolveUpdateState","updateStateLabel","OrgPluginInstallsCard","props","orgId","orgSlug","hosts","firestore","data","orgInstalls","idField","hostIdsKey","map","host","id","join","sitePins","setSitePins","length","active","Promise","all","snapshot","docs","entry","$id","then","entries","Object","fromEntries","catch","undefined","installations","byListing","Map","install","set","siteLabels","orgWide","pin","existing","get","push","label","values","listingIdsKey","sort","listings","setListings","revocations","setRevocations","ids","split","chunks","index","slice","fetchChunks","path","chunk","results","flatMap","header","help","anchor","contentGutterX","contentGutterY","status","InstallationRow","href","ORG_PLUGIN_INSTALLATION","pluginRef","displayName","pluginId","caption","version","trailing","state","size","color","variant","availableVersion","title","severity","underline","direction","spacing","sx","alignItems","py","px","mx","borderRadius","bgcolor","flex","minWidth","noWrap","fontSize"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/org-plugin-installs-card.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 {\n buildRoute,\n pluginDocsHelp,\n Route,\n type ConsoleOrgPluginInstallsZoneProps,\n} from '@aglyn/aglyn'\nimport { mdiChevronRight } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Alert, Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n documentId,\n getDocs,\n limit,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState, type ReactNode } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { installedPluginCaption } from '../model/marketplace-catalog'\nimport { resolveUpdateState, updateStateLabel } from '../model/update-state'\n\n/** One installation: its org pin, or the sites that pin it. */\ninterface Installation {\n $id: string\n displayName?: string\n pluginId?: string\n version?: string\n siteLabels: string[]\n orgWide: boolean\n}\n\n/**\n * \"Installed from the marketplace\", on the workspace's Plugins page — the\n * `orgPluginInstalls` zone (AGL-1011, drawn here since AGL-3080).\n *\n * The page is the workspace's inventory, first-party and marketplace\n * together, because where a plugin came from is a fact about it rather than a\n * different kind of thing. The built-in half is the shell's. This half is the\n * marketplace's: the pins are its `installs`, and the update chip compares\n * them with its listings and its kill switches — so the page hands over the\n * sites it can see and this card draws the rest.\n *\n * One row per INSTALLATION, not per pin: a plugin on three sites is one thing\n * an admin manages, and the scope is a fact about it rather than three\n * separate entries. An org pin covers every site, so it wins the caption\n * outright. Every row goes to the shell's installation page, which is where\n * an installation is managed.\n */\nexport function OrgPluginInstallsCard(props: ConsoleOrgPluginInstallsZoneProps) {\n const { orgId, orgSlug, hosts } = props\n const firestore = useFirestore()\n\n // Held at null while the org resolves, never `orgs/-pending-` (AGL-1440):\n // installs are member-gated, so the sentinel was a guaranteed-denied listen\n // on every mount.\n const { data: orgInstalls } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(collection(firestore, 'orgs', orgId, 'installs'), limit(100))\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n\n // Site pins too (AGL-1012). Reading only the org pins meant a plugin\n // targeted at specific sites — installed, running, and the whole point of\n // AGL-773/997 — was absent from the workspace's own plugin inventory.\n // Fan-in per site: the host count is data, so a hook per host would change\n // the hook count between renders.\n const hostIdsKey = hosts.map((host) => host.id).join('|')\n const [sitePins, setSitePins] = useState<Record<string, any[]>>({})\n useEffect(() => {\n if (!hosts.length) return\n let active = true\n void Promise.all(\n hosts.map(async (host) => {\n const snapshot = await getDocs(\n query(collection(firestore, 'hosts', host.id, 'installs'), limit(100)),\n )\n return [\n host.id,\n snapshot.docs.map((entry) => ({ $id: entry.id, ...entry.data() })),\n ] as const\n }),\n )\n .then((entries) => {\n if (active) setSitePins(Object.fromEntries(entries))\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, hostIdsKey])\n\n const installations = useMemo(() => {\n const byListing = new Map<string, Installation>()\n for (const install of orgInstalls ?? []) {\n byListing.set(install.$id, { ...install, siteLabels: [], orgWide: true })\n }\n for (const host of hosts) {\n for (const pin of sitePins[host.id] ?? []) {\n const existing = byListing.get(pin.$id)\n if (existing) existing.siteLabels.push(host.label)\n else\n byListing.set(pin.$id, {\n ...pin,\n siteLabels: [host.label],\n orgWide: false,\n })\n }\n }\n return [...byListing.values()]\n }, [orgInstalls, hosts, sitePins])\n\n /**\n * The listings behind those installs, for the update chip (AGL-1016), and\n * their kill switches (AGL-2368) — a revoked version stays `approved`,\n * because revocation does not clear a review verdict, so without them the\n * chip offered an update `install-plugin` answers 409 to.\n *\n * Both are public reads, so plain fetches — but `in` caps at 30 ids, and a\n * workspace can run more plugins than that, so they chunk. A failure leaves\n * a map empty and every row resolves to `unknown`, which reads as \"we can't\n * say\" rather than a false \"up to date\".\n */\n const listingIdsKey = installations\n .map((install) => install.$id)\n .sort()\n .join('|')\n const [listings, setListings] = useState<Record<string, any>>({})\n const [revocations, setRevocations] = useState<Record<string, any>>({})\n useEffect(() => {\n const ids = listingIdsKey ? listingIdsKey.split('|') : []\n if (!ids.length) return\n let active = true\n const chunks: string[][] = []\n for (let index = 0; index < ids.length; index += 30) {\n chunks.push(ids.slice(index, index + 30))\n }\n const fetchChunks = (path: string) =>\n Promise.all(\n chunks.map((chunk) =>\n getDocs(\n query(collection(firestore, path), where(documentId(), 'in', chunk)),\n ),\n ),\n ).then((results) =>\n Object.fromEntries(\n results.flatMap((snapshot) =>\n snapshot.docs.map((entry) => [entry.id, entry.data()]),\n ),\n ),\n )\n void fetchChunks('marketplaceListings')\n .then((map) => {\n if (active) setListings(map)\n })\n .catch(() => undefined)\n void fetchChunks('revocations')\n .then((map) => {\n if (active) setRevocations(map)\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, listingIdsKey])\n\n return (\n <CardDisplay\n header={'Installed from the marketplace'}\n // Card-level help, not just the page's (AGL-2129). This is the card\n // people arrive at looking for a switch, having read in Marketplace\n // that installing is done there — so it points at the step of the\n // walkthrough that explains the split.\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#installed-from-the-marketplace',\n })}\n contentGutterX\n contentGutterY\n >\n {installations.length ? (\n <Stack>\n {installations.map((install) => {\n // The update signal (AGL-1016/2368), from the one shared\n // comparison — for plugins that is the newest INSTALLABLE\n // version, so this can never offer what install refuses.\n const status = resolveUpdateState(\n install as never,\n listings[install.$id] ?? null,\n 'plugin',\n revocations[install.$id] ?? null,\n )\n return (\n <InstallationRow\n key={install.$id}\n href={buildRoute(Route.ORG_PLUGIN_INSTALLATION, {\n orgSlug,\n pluginRef: install.$id,\n })}\n label={install.displayName ?? install.pluginId ?? install.$id}\n caption={installedPluginCaption(install)}\n trailing={\n status.state === 'update-available' ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n label={`v${status.availableVersion} available`}\n />\n ) : status.state === 'unknown' ? (\n <Tooltip title={updateStateLabel(status)}>\n <Chip size=\"small\" variant=\"outlined\" label={'Unknown'} />\n </Tooltip>\n ) : undefined\n }\n />\n )\n })}\n </Stack>\n ) : (\n <Alert severity=\"info\">\n {'Nothing installed from the marketplace yet — browse it to add one.'}\n </Alert>\n )}\n </CardDisplay>\n )\n}\n\n/**\n * One row, drawn the way the page draws its built-in rows: the whole row is\n * the link and a chevron says so, because the entry point AGL-1011 replaced\n * was a name rendered as body text that nothing said went anywhere.\n */\nfunction InstallationRow(props: {\n href: string\n label: string\n caption: string\n trailing?: ReactNode\n}) {\n const { href, label, caption, trailing } = props\n return (\n <AppLink href={href} color=\"inherit\" underline=\"none\">\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n py: 1.25,\n px: 1,\n mx: -1,\n borderRadius: 1,\n '&:hover': { bgcolor: 'action.hover' },\n }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {caption}\n </Typography>\n </Stack>\n {trailing}\n <MdiIcon path={mdiChevronRight.path} color=\"disabled\" sx={{ fontSize: 20 }} />\n </Stack>\n </AppLink>\n )\n}\n\nexport default OrgPluginInstallsCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiChevronRight","AppLink","CardDisplay","MdiIcon","Alert","Chip","Stack","Tooltip","Typography","collection","documentId","getDocs","limit","query","where","useEffect","useMemo","useState","useFirestore","useFirestoreCollection","installedPluginCaption","resolveUpdateState","updateStateLabel","OrgPluginInstallsCard","props","orgId","orgSlug","hosts","firestore","data","orgInstalls","idField","hostIdsKey","map","host","id","join","sitePins","setSitePins","length","active","Promise","all","snapshot","docs","entry","$id","then","entries","Object","fromEntries","catch","undefined","installations","byListing","Map","install","set","siteLabels","orgWide","pin","existing","get","push","label","values","listingIdsKey","sort","listings","setListings","revocations","setRevocations","ids","split","chunks","index","slice","fetchChunks","path","chunk","results","flatMap","header","help","anchor","contentGutterX","contentGutterY","status","InstallationRow","href","ORG_PLUGIN_INSTALLATION","pluginRef","displayName","pluginId","caption","trailing","state","size","color","variant","availableVersion","title","severity","underline","direction","spacing","sx","alignItems","py","px","mx","borderRadius","bgcolor","flex","minWidth","noWrap","fontSize"],"mappings":"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"}
|
|
@@ -20,8 +20,8 @@ import { mdiCheckCircle } from "@aglyn/shared-data-mdi";
|
|
|
20
20
|
import { Alert, Box, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControlLabel, FormGroup, Stack, Typography } from "@mui/material";
|
|
21
21
|
import { useEffect, useMemo, useState } from "react";
|
|
22
22
|
import { resolveUninstallTargets } from "../model/marketplace.js";
|
|
23
|
+
import { PLUGIN_ORG_WIDE_UPDATE_NOTE, pluginSiteSetSummary, pluginUpdateAll, pluginUpdatePrompt, staleSites as staleSitesOf } from "../model/marketplace-catalog.js";
|
|
23
24
|
import UninstallImpactDialog from "./uninstall-impact-dialog.component.js";
|
|
24
|
-
import { compareArtifactVersions } from "../model/update-state.js";
|
|
25
25
|
/**
|
|
26
26
|
* The set of sites a plugin is installed on, and the controls to change it
|
|
27
27
|
* (AGL-997), extracted from the listing page so the installation detail page
|
|
@@ -48,13 +48,11 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
48
48
|
* equal" also fires when a site runs something newer than the offer — a
|
|
49
49
|
* publisher testing their own build, or a version withdrawn after install —
|
|
50
50
|
* and offering to "update" those would quietly move them backwards.
|
|
51
|
-
*/ const staleSites = useMemo(()=>orgInstall.sites
|
|
52
|
-
var _compareArtifactVersions;
|
|
53
|
-
return ((_compareArtifactVersions = compareArtifactVersions(site.version, latestVersion)) != null ? _compareArtifactVersions : 0) < 0;
|
|
54
|
-
}), [
|
|
51
|
+
*/ const staleSites = useMemo(()=>staleSitesOf(orgInstall.sites, latestVersion), [
|
|
55
52
|
orgInstall.sites,
|
|
56
53
|
latestVersion
|
|
57
54
|
]);
|
|
55
|
+
const updateAll = pluginUpdateAll(orgInstall, staleSites, String(latestVersion));
|
|
58
56
|
/**
|
|
59
57
|
* Update is per-site FIRST (AGL-1017). A version with breaking changes has
|
|
60
58
|
* to be absorbable one site at a time — take it on a quiet site, see what
|
|
@@ -66,6 +64,12 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
66
64
|
* button that looks per-site, the panel says so and points at the split.
|
|
67
65
|
*/ const [pending, setPending] = useState(null);
|
|
68
66
|
const [changelog, setChangelog] = useState(null);
|
|
67
|
+
const updatePrompt = pluginUpdatePrompt({
|
|
68
|
+
name: (_ref = listing == null ? void 0 : listing.displayName) != null ? _ref : 'This plugin',
|
|
69
|
+
latestVersion: String(latestVersion),
|
|
70
|
+
label: (_ref1 = pending == null ? void 0 : pending.label) != null ? _ref1 : '',
|
|
71
|
+
singleSite: Boolean((pending == null ? void 0 : pending.hostIds) && pending.hostIds.length === 1)
|
|
72
|
+
});
|
|
69
73
|
useEffect(()=>{
|
|
70
74
|
if (!pending || !(listing == null ? void 0 : listing.$id) || !latestVersion) return;
|
|
71
75
|
let active = true;
|
|
@@ -117,7 +121,7 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
117
121
|
}),
|
|
118
122
|
/*#__PURE__*/ _jsx(Typography, {
|
|
119
123
|
variant: "body2",
|
|
120
|
-
children:
|
|
124
|
+
children: pluginSiteSetSummary(orgInstall, hosts.length)
|
|
121
125
|
})
|
|
122
126
|
]
|
|
123
127
|
}),
|
|
@@ -189,7 +193,7 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
189
193
|
/*#__PURE__*/ _jsx(Alert, {
|
|
190
194
|
severity: "info",
|
|
191
195
|
variant: "outlined",
|
|
192
|
-
children:
|
|
196
|
+
children: PLUGIN_ORG_WIDE_UPDATE_NOTE
|
|
193
197
|
}) : null,
|
|
194
198
|
/*#__PURE__*/ _jsx(Box, {
|
|
195
199
|
children: /*#__PURE__*/ _jsx(Button, {
|
|
@@ -198,10 +202,10 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
198
202
|
color: "primary",
|
|
199
203
|
disabled: siteBusy,
|
|
200
204
|
onClick: ()=>setPending({
|
|
201
|
-
hostIds:
|
|
202
|
-
label:
|
|
205
|
+
hostIds: updateAll.hostIds,
|
|
206
|
+
label: updateAll.target
|
|
203
207
|
}),
|
|
204
|
-
children:
|
|
208
|
+
children: updateAll.label
|
|
205
209
|
})
|
|
206
210
|
})
|
|
207
211
|
]
|
|
@@ -213,7 +217,7 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
213
217
|
fullWidth: true,
|
|
214
218
|
children: [
|
|
215
219
|
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
216
|
-
children:
|
|
220
|
+
children: updatePrompt.title
|
|
217
221
|
}),
|
|
218
222
|
/*#__PURE__*/ _jsx(DialogContent, {
|
|
219
223
|
dividers: true,
|
|
@@ -222,12 +226,12 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
222
226
|
children: [
|
|
223
227
|
/*#__PURE__*/ _jsx(Typography, {
|
|
224
228
|
variant: "body2",
|
|
225
|
-
children:
|
|
229
|
+
children: updatePrompt.body
|
|
226
230
|
}),
|
|
227
|
-
|
|
231
|
+
updatePrompt.others ? /*#__PURE__*/ _jsx(Typography, {
|
|
228
232
|
variant: "body2",
|
|
229
233
|
color: "text.secondary",
|
|
230
|
-
children:
|
|
234
|
+
children: updatePrompt.others
|
|
231
235
|
}) : null,
|
|
232
236
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
233
237
|
spacing: 0.5,
|
|
@@ -256,7 +260,7 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
256
260
|
/*#__PURE__*/ _jsx(Typography, {
|
|
257
261
|
variant: "caption",
|
|
258
262
|
color: "text.secondary",
|
|
259
|
-
children:
|
|
263
|
+
children: updatePrompt.note
|
|
260
264
|
})
|
|
261
265
|
]
|
|
262
266
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/plugin-site-set.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 { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { mdiCheckCircle } from '@aglyn/shared-data-mdi'\nimport {\n Alert,\n Box,\n Button,\n Checkbox,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n FormControlLabel,\n FormGroup,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\n\nimport {\n resolveUninstallTargets,\n type OrgInstallSummary,\n type UninstallTarget,\n} from '../model/marketplace'\nimport UninstallImpactDialog from './uninstall-impact-dialog.component'\nimport { compareArtifactVersions } from '../model/update-state'\n\nexport interface PluginSiteSetProps {\n /** The listing/installation these pins belong to. */\n listing: any\n /** Every site in the org, in display order. */\n hosts: ReadonlyArray<{ id: string; label: string }>\n /** The resolved picture — see `resolveOrgInstallSummary`. */\n orgInstall: OrgInstallSummary\n /** Version the pins should be at; a site behind this can be updated. */\n latestVersion?: string | number | null\n /** Install the given concrete steps (org pin and/or named host pins). */\n installPlan: (\n listing: any,\n steps: ReadonlyArray<{ scope: 'org' | 'host'; hostId?: string }>,\n options?: { intent?: 'install' | 'update'; version?: string | number | null },\n ) => Promise<unknown>\n /** Drop a pin — org-wide, or from one named site. */\n uninstall: (\n listing: any,\n scope?: 'org' | 'host',\n hostId?: string,\n ) => Promise<unknown>\n /** Re-read the pins after a write; the caller owns that state. */\n onChanged: () => void\n}\n\n/**\n * The set of sites a plugin is installed on, and the controls to change it\n * (AGL-997), extracted from the listing page so the installation detail page\n * (AGL-1007) shows the same thing rather than a second implementation that\n * drifts.\n *\n * An org-scope install is a SET, not a boolean: an org pin covers every site\n * including ones created later, host pins name specific sites, and a host\n * pin on top of an org pin shadows it for that one site. All three facts\n * have to survive into the UI, which is why this takes the whole summary\n * rather than a scope.\n */\nexport function PluginSiteSet(props: PluginSiteSetProps) {\n const {\n listing,\n hosts,\n orgInstall,\n latestVersion,\n installPlan,\n uninstall,\n onChanged,\n } = props\n const [addHostIds, setAddHostIds] = useState<string[]>([])\n /**\n * The uninstall awaiting confirmation (AGL-1027). Uninstalling used to be a\n * bare click; nothing said which sites lost the plugin or which published\n * pages stopped rendering it.\n */\n const [pendingUninstall, setPendingUninstall] = useState<{\n scope: 'org' | 'host'\n hostId?: string\n targets: UninstallTarget[]\n } | null>(null)\n const [siteBusy, setSiteBusy] = useState(false)\n /**\n * A site is behind only when its pin is genuinely OLDER (AGL-1017). \"Not\n * equal\" also fires when a site runs something newer than the offer — a\n * publisher testing their own build, or a version withdrawn after install —\n * and offering to \"update\" those would quietly move them backwards.\n */\n const staleSites = useMemo(\n () =>\n orgInstall.sites.filter(\n (site) => (compareArtifactVersions(site.version, latestVersion) ?? 0) < 0,\n ),\n [orgInstall.sites, latestVersion],\n )\n /**\n * Update is per-site FIRST (AGL-1017). A version with breaking changes has\n * to be absorbable one site at a time — take it on a quiet site, see what\n * breaks, then decide about the rest — so each site behind the offer gets\n * its own control and the bulk action is the convenience on top.\n *\n * An org pin cannot do that: it is one pointer covering every site, so\n * moving it moves all of them at once. Rather than hide that behind a\n * button that looks per-site, the panel says so and points at the split.\n */\n const [pending, setPending] = useState<\n { hostIds: string[] | null; label: string } | null\n >(null)\n const [changelog, setChangelog] = useState<string | null>(null)\n useEffect(() => {\n if (!pending || !listing?.$id || !latestVersion) return\n let active = true\n setChangelog(null)\n void fetch(\n `/api/marketplace/listing-versions?listingId=${encodeURIComponent(\n String(listing.$id),\n )}`,\n )\n .then((response) => (response.ok ? response.json() : { versions: [] }))\n .then((payload) => {\n if (!active) return\n const entry = (payload?.versions ?? []).find(\n (version: { version?: string }) =>\n String(version?.version) === String(latestVersion),\n )\n setChangelog(entry?.changelog ?? '')\n })\n .catch(() => active && setChangelog(''))\n return () => {\n active = false\n }\n }, [pending, listing?.$id, latestVersion])\n const runSiteEdit = async (work: () => Promise<unknown>) => {\n setSiteBusy(true)\n try {\n await work()\n } finally {\n setSiteBusy(false)\n setAddHostIds([])\n onChanged()\n }\n }\n\n if (!orgInstall.installedAnywhere) return null\n\n return (\n <Stack spacing={1.5}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <MdiIcon\n path={mdiCheckCircle.path}\n color=\"success\"\n sx={{ fontSize: 20, mt: '2px' }}\n />\n <Typography variant=\"body2\">\n {orgInstall.orgWide\n ? `Installed for the whole organization ` +\n `(v${orgInstall.orgVersion}) — every site, ` +\n 'including ones you add later.'\n : `Installed on ${orgInstall.sites.length} of ` +\n `${hosts.length} sites.`}\n </Typography>\n </Stack>\n\n {/* Name them. A count alone still leaves you\n guessing which sites are running the thing. */}\n <Stack spacing={0.5}>\n {orgInstall.sites.map((site) => (\n <Stack\n key={site.hostId}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography\n variant=\"body2\"\n sx={{ flex: 1, minWidth: 0 }}\n noWrap\n >\n {site.label}\n </Typography>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n >\n {`v${site.version ?? '—'}`}\n </Typography>\n {site.shadowed ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Overrides org\"\n />\n ) : null}\n {/* Removable only where the site owns its\n pin: dropping a site covered by the org\n pin would mean writing an exclusion the\n loader has no concept of. */}\n {/* This site alone (AGL-1017). Only where the site owns its pin —\n a site covered by the org pin has no pointer of its own to\n move, and pretending otherwise would update every site. */}\n {site.pinnedBy === 'host' &&\n staleSites.some((stale) => stale.hostId === site.hostId) ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy}\n onClick={() =>\n setPending({\n hostIds: [site.hostId],\n label: site.label,\n })\n }\n >\n {`Update to v${latestVersion}`}\n </Button>\n ) : null}\n {site.pinnedBy === 'host' ? (\n <Button\n size=\"small\"\n color=\"inherit\"\n disabled={siteBusy}\n onClick={() =>\n setPendingUninstall({\n scope: 'host',\n hostId: site.hostId,\n targets: resolveUninstallTargets(\n orgInstall,\n 'host',\n site.hostId,\n ),\n })\n }\n >\n {'Remove'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n\n {staleSites.length ? (\n <Stack spacing={1}>\n {orgInstall.orgWide ? (\n // Said out loud rather than discovered afterwards (AGL-1017): an\n // org pin is a single pointer, so there is no half of it to move.\n // A workspace that wants to stage a breaking update has to be on\n // per-site pins, and the way there is the split below.\n <Alert severity=\"info\" variant=\"outlined\">\n {'This is one organization-wide pin, so updating moves every ' +\n 'site at once. To take a version on one site first, install ' +\n 'per site instead.'}\n </Alert>\n ) : null}\n <Box>\n <Button\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy}\n onClick={() =>\n setPending({\n hostIds: orgInstall.orgWide ? null : staleSites.map((site) => site.hostId),\n label: orgInstall.orgWide\n ? 'every site in this organization'\n : `${staleSites.length} site${staleSites.length === 1 ? '' : 's'}`,\n })\n }\n >\n {orgInstall.orgWide\n ? `Update to v${latestVersion}`\n : `Update all ${staleSites.length} to v${latestVersion}`}\n </Button>\n </Box>\n </Stack>\n ) : null}\n\n {/* What changes, before it changes (AGL-1017). Third-party code moving\n in a workspace deserves more than a spinner: the version pair, the\n publisher's changelog, and which sites are about to take it. */}\n <Dialog\n open={Boolean(pending)}\n onClose={() => setPending(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{`Update to v${latestVersion}?`}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={1.5}>\n <Typography variant=\"body2\">\n {`${listing?.displayName ?? 'This plugin'} moves from the version ` +\n `running now to v${latestVersion} on ${pending?.label ?? ''}.`}\n </Typography>\n {pending?.hostIds && pending.hostIds.length === 1 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Other sites stay on the version they are running.'}\n </Typography>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{\"What's new\"}</Typography>\n {changelog === null ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading the changelog…'}\n </Typography>\n ) : changelog ? (\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap' }}>\n {changelog}\n </Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The publisher did not describe this version.'}\n </Typography>\n )}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Pins only move forward — there is no going back to the ' +\n 'previous version from here.'}\n </Typography>\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button color=\"inherit\" onClick={() => setPending(null)}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={siteBusy}\n onClick={() => {\n const target = pending\n setPending(null)\n if (!target) return\n void runSiteEdit(() =>\n installPlan(\n listing,\n target.hostIds\n ? target.hostIds.map((hostId) => ({\n scope: 'host' as const,\n hostId,\n }))\n : [{ scope: 'org' as const }],\n { intent: 'update', version: latestVersion },\n ),\n )\n }}\n >\n {'Update'}\n </Button>\n </DialogActions>\n </Dialog>\n\n {/* Add sites after the fact. The picker used to\n render only when nothing was installed, which\n froze the set at install time. */}\n {!orgInstall.orgWide &&\n orgInstall.availableHostIds.length ? (\n <Stack spacing={0.5}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n >\n {'Add to more sites'}\n </Typography>\n <FormGroup\n sx={{ maxHeight: 200, overflowY: 'auto' }}\n >\n {orgInstall.availableHostIds.map((hostIdOption) => (\n <FormControlLabel\n key={hostIdOption}\n control={\n <Checkbox\n size=\"small\"\n checked={addHostIds.includes(\n hostIdOption,\n )}\n onChange={(event) =>\n setAddHostIds((current) =>\n event.target.checked\n ? [...current, hostIdOption]\n : current.filter(\n (id) => id !== hostIdOption,\n ),\n )\n }\n />\n }\n label={\n hosts.find(\n (host) => host.id === hostIdOption,\n )?.label ?? hostIdOption\n }\n slotProps={{\n typography: { variant: 'body2' },\n }}\n />\n ))}\n </FormGroup>\n <Box>\n <Button\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy || !addHostIds.length}\n onClick={() =>\n void runSiteEdit(() =>\n installPlan(\n listing,\n addHostIds.map((hostIdOption) => ({\n scope: 'host' as const,\n hostId: hostIdOption,\n })),\n ),\n )\n }\n >\n {'Add to selected'}\n </Button>\n </Box>\n </Stack>\n ) : null}\n\n <Divider />\n\n {/* Change the shape of the install, not just its\n membership: one org pin covers sites made\n later, which no set of host pins can. */}\n {orgInstall.orgWide ? (\n <>\n {/* The inverse of promote (AGL-1017), and the thing that makes\n per-site updates reachable at all: one org pin cannot be moved\n for one site, so a workspace that needs to stage a breaking\n version has to hold per-site pins first. Sites added later are\n no longer covered automatically — that is the trade, and the\n button says so rather than discovering it weeks later. */}\n <Button\n fullWidth\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy || !hosts.length}\n onClick={() =>\n void runSiteEdit(async () => {\n // Pin every site BEFORE dropping the org pin, so a failure\n // part-way leaves the plugin installed everywhere it already\n // was rather than nowhere.\n await installPlan(\n listing,\n hosts.map((host) => ({ scope: 'host' as const, hostId: host.id })),\n )\n await uninstall(listing, 'org')\n })\n }\n >\n {'Split into per-site installs'}\n </Button>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Pins each site separately so you can update them one at a time. ' +\n 'Sites you add later will not get this plugin automatically.'}\n </Typography>\n <Button\n fullWidth\n size=\"small\"\n color=\"inherit\"\n disabled={siteBusy}\n onClick={() =>\n setPendingUninstall({\n scope: 'org',\n targets: resolveUninstallTargets(orgInstall, 'org'),\n })\n }\n >\n {'Uninstall org-wide'}\n </Button>\n </>\n ) : (\n <Button\n fullWidth\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy}\n onClick={() =>\n void runSiteEdit(async () => {\n // Promote: add the org pin, then drop the\n // per-site pins it makes redundant. In\n // this order, so a failure part-way\n // leaves the plugin installed everywhere\n // it already was rather than nowhere.\n await installPlan(listing, [\n { scope: 'org' },\n ])\n for (const hostIdPinned of orgInstall.hostPinnedIds) {\n await uninstall(\n listing,\n 'host',\n hostIdPinned,\n )\n }\n })\n }\n >\n {'Install for the whole organization'}\n </Button>\n )}\n\n {/* Only the two DIRECT uninstalls are confirmed. The promote and split\n flows above also call `uninstall`, but there the removal is one half of\n a move that leaves the plugin installed everywhere it already was —\n confirming \"this will stop working\" would be false. */}\n <UninstallImpactDialog\n open={pendingUninstall !== null}\n listing={listing}\n scope={pendingUninstall?.scope ?? 'host'}\n targets={pendingUninstall?.targets ?? []}\n onCancel={() => setPendingUninstall(null)}\n onConfirm={async () => {\n const target = pendingUninstall\n setPendingUninstall(null)\n if (!target) return\n await runSiteEdit(() =>\n uninstall(listing, target.scope, target.hostId),\n )\n }}\n />\n </Stack>\n )\n}\nPluginSiteSet.displayName = 'PluginSiteSet'\n\nexport default PluginSiteSet\n"],"names":["MdiIcon","mdiCheckCircle","Alert","Box","Button","Checkbox","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Divider","FormControlLabel","FormGroup","Stack","Typography","useEffect","useMemo","useState","resolveUninstallTargets","UninstallImpactDialog","compareArtifactVersions","PluginSiteSet","props","listing","hosts","orgInstall","latestVersion","installPlan","uninstall","onChanged","addHostIds","setAddHostIds","pendingUninstall","setPendingUninstall","siteBusy","setSiteBusy","staleSites","sites","filter","site","version","pending","setPending","changelog","setChangelog","$id","active","fetch","encodeURIComponent","String","then","response","ok","json","versions","payload","entry","find","catch","runSiteEdit","work","installedAnywhere","spacing","direction","sx","alignItems","path","color","fontSize","mt","variant","orgWide","orgVersion","length","map","flex","minWidth","noWrap","label","shadowed","size","pinnedBy","some","stale","hostId","disabled","onClick","hostIds","scope","targets","severity","open","Boolean","onClose","maxWidth","fullWidth","dividers","displayName","whiteSpace","target","intent","availableHostIds","maxHeight","overflowY","hostIdOption","control","checked","includes","onChange","event","current","id","host","slotProps","typography","hostIdPinned","hostPinnedIds","onCancel","onConfirm"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/plugin-site-set.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 { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { mdiCheckCircle } from '@aglyn/shared-data-mdi'\nimport {\n Alert,\n Box,\n Button,\n Checkbox,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n FormControlLabel,\n FormGroup,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\n\nimport {\n resolveUninstallTargets,\n type OrgInstallSummary,\n type UninstallTarget,\n} from '../model/marketplace'\nimport {\n PLUGIN_ORG_WIDE_UPDATE_NOTE,\n pluginSiteSetSummary,\n pluginUpdateAll,\n pluginUpdatePrompt,\n staleSites as staleSitesOf,\n} from '../model/marketplace-catalog'\nimport UninstallImpactDialog from './uninstall-impact-dialog.component'\n\nexport interface PluginSiteSetProps {\n /** The listing/installation these pins belong to. */\n listing: any\n /** Every site in the org, in display order. */\n hosts: ReadonlyArray<{ id: string; label: string }>\n /** The resolved picture — see `resolveOrgInstallSummary`. */\n orgInstall: OrgInstallSummary\n /** Version the pins should be at; a site behind this can be updated. */\n latestVersion?: string | number | null\n /** Install the given concrete steps (org pin and/or named host pins). */\n installPlan: (\n listing: any,\n steps: ReadonlyArray<{ scope: 'org' | 'host'; hostId?: string }>,\n options?: { intent?: 'install' | 'update'; version?: string | number | null },\n ) => Promise<unknown>\n /** Drop a pin — org-wide, or from one named site. */\n uninstall: (\n listing: any,\n scope?: 'org' | 'host',\n hostId?: string,\n ) => Promise<unknown>\n /** Re-read the pins after a write; the caller owns that state. */\n onChanged: () => void\n}\n\n/**\n * The set of sites a plugin is installed on, and the controls to change it\n * (AGL-997), extracted from the listing page so the installation detail page\n * (AGL-1007) shows the same thing rather than a second implementation that\n * drifts.\n *\n * An org-scope install is a SET, not a boolean: an org pin covers every site\n * including ones created later, host pins name specific sites, and a host\n * pin on top of an org pin shadows it for that one site. All three facts\n * have to survive into the UI, which is why this takes the whole summary\n * rather than a scope.\n */\nexport function PluginSiteSet(props: PluginSiteSetProps) {\n const {\n listing,\n hosts,\n orgInstall,\n latestVersion,\n installPlan,\n uninstall,\n onChanged,\n } = props\n const [addHostIds, setAddHostIds] = useState<string[]>([])\n /**\n * The uninstall awaiting confirmation (AGL-1027). Uninstalling used to be a\n * bare click; nothing said which sites lost the plugin or which published\n * pages stopped rendering it.\n */\n const [pendingUninstall, setPendingUninstall] = useState<{\n scope: 'org' | 'host'\n hostId?: string\n targets: UninstallTarget[]\n } | null>(null)\n const [siteBusy, setSiteBusy] = useState(false)\n /**\n * A site is behind only when its pin is genuinely OLDER (AGL-1017). \"Not\n * equal\" also fires when a site runs something newer than the offer — a\n * publisher testing their own build, or a version withdrawn after install —\n * and offering to \"update\" those would quietly move them backwards.\n */\n const staleSites = useMemo(\n () => staleSitesOf(orgInstall.sites, latestVersion),\n [orgInstall.sites, latestVersion],\n )\n const updateAll = pluginUpdateAll(orgInstall, staleSites, String(latestVersion))\n /**\n * Update is per-site FIRST (AGL-1017). A version with breaking changes has\n * to be absorbable one site at a time — take it on a quiet site, see what\n * breaks, then decide about the rest — so each site behind the offer gets\n * its own control and the bulk action is the convenience on top.\n *\n * An org pin cannot do that: it is one pointer covering every site, so\n * moving it moves all of them at once. Rather than hide that behind a\n * button that looks per-site, the panel says so and points at the split.\n */\n const [pending, setPending] = useState<\n { hostIds: string[] | null; label: string } | null\n >(null)\n const [changelog, setChangelog] = useState<string | null>(null)\n const updatePrompt = pluginUpdatePrompt({\n name: listing?.displayName ?? 'This plugin',\n latestVersion: String(latestVersion),\n label: pending?.label ?? '',\n singleSite: Boolean(pending?.hostIds && pending.hostIds.length === 1),\n })\n useEffect(() => {\n if (!pending || !listing?.$id || !latestVersion) return\n let active = true\n setChangelog(null)\n void fetch(\n `/api/marketplace/listing-versions?listingId=${encodeURIComponent(\n String(listing.$id),\n )}`,\n )\n .then((response) => (response.ok ? response.json() : { versions: [] }))\n .then((payload) => {\n if (!active) return\n const entry = (payload?.versions ?? []).find(\n (version: { version?: string }) =>\n String(version?.version) === String(latestVersion),\n )\n setChangelog(entry?.changelog ?? '')\n })\n .catch(() => active && setChangelog(''))\n return () => {\n active = false\n }\n }, [pending, listing?.$id, latestVersion])\n const runSiteEdit = async (work: () => Promise<unknown>) => {\n setSiteBusy(true)\n try {\n await work()\n } finally {\n setSiteBusy(false)\n setAddHostIds([])\n onChanged()\n }\n }\n\n if (!orgInstall.installedAnywhere) return null\n\n return (\n <Stack spacing={1.5}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <MdiIcon\n path={mdiCheckCircle.path}\n color=\"success\"\n sx={{ fontSize: 20, mt: '2px' }}\n />\n <Typography variant=\"body2\">\n {pluginSiteSetSummary(orgInstall, hosts.length)}\n </Typography>\n </Stack>\n\n {/* Name them. A count alone still leaves you\n guessing which sites are running the thing. */}\n <Stack spacing={0.5}>\n {orgInstall.sites.map((site) => (\n <Stack\n key={site.hostId}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography\n variant=\"body2\"\n sx={{ flex: 1, minWidth: 0 }}\n noWrap\n >\n {site.label}\n </Typography>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n >\n {`v${site.version ?? '—'}`}\n </Typography>\n {site.shadowed ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Overrides org\"\n />\n ) : null}\n {/* Removable only where the site owns its\n pin: dropping a site covered by the org\n pin would mean writing an exclusion the\n loader has no concept of. */}\n {/* This site alone (AGL-1017). Only where the site owns its pin —\n a site covered by the org pin has no pointer of its own to\n move, and pretending otherwise would update every site. */}\n {site.pinnedBy === 'host' &&\n staleSites.some((stale) => stale.hostId === site.hostId) ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy}\n onClick={() =>\n setPending({\n hostIds: [site.hostId],\n label: site.label,\n })\n }\n >\n {`Update to v${latestVersion}`}\n </Button>\n ) : null}\n {site.pinnedBy === 'host' ? (\n <Button\n size=\"small\"\n color=\"inherit\"\n disabled={siteBusy}\n onClick={() =>\n setPendingUninstall({\n scope: 'host',\n hostId: site.hostId,\n targets: resolveUninstallTargets(\n orgInstall,\n 'host',\n site.hostId,\n ),\n })\n }\n >\n {'Remove'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n\n {staleSites.length ? (\n <Stack spacing={1}>\n {orgInstall.orgWide ? (\n // Said out loud rather than discovered afterwards (AGL-1017): an\n // org pin is a single pointer, so there is no half of it to move.\n // A workspace that wants to stage a breaking update has to be on\n // per-site pins, and the way there is the split below.\n <Alert severity=\"info\" variant=\"outlined\">\n {PLUGIN_ORG_WIDE_UPDATE_NOTE}\n </Alert>\n ) : null}\n <Box>\n <Button\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy}\n onClick={() =>\n setPending({ hostIds: updateAll.hostIds, label: updateAll.target })\n }\n >\n {updateAll.label}\n </Button>\n </Box>\n </Stack>\n ) : null}\n\n {/* What changes, before it changes (AGL-1017). Third-party code moving\n in a workspace deserves more than a spinner: the version pair, the\n publisher's changelog, and which sites are about to take it. */}\n <Dialog\n open={Boolean(pending)}\n onClose={() => setPending(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{updatePrompt.title}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={1.5}>\n <Typography variant=\"body2\">{updatePrompt.body}</Typography>\n {updatePrompt.others ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {updatePrompt.others}\n </Typography>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{\"What's new\"}</Typography>\n {changelog === null ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading the changelog…'}\n </Typography>\n ) : changelog ? (\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap' }}>\n {changelog}\n </Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The publisher did not describe this version.'}\n </Typography>\n )}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {updatePrompt.note}\n </Typography>\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button color=\"inherit\" onClick={() => setPending(null)}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={siteBusy}\n onClick={() => {\n const target = pending\n setPending(null)\n if (!target) return\n void runSiteEdit(() =>\n installPlan(\n listing,\n target.hostIds\n ? target.hostIds.map((hostId) => ({\n scope: 'host' as const,\n hostId,\n }))\n : [{ scope: 'org' as const }],\n { intent: 'update', version: latestVersion },\n ),\n )\n }}\n >\n {'Update'}\n </Button>\n </DialogActions>\n </Dialog>\n\n {/* Add sites after the fact. The picker used to\n render only when nothing was installed, which\n froze the set at install time. */}\n {!orgInstall.orgWide &&\n orgInstall.availableHostIds.length ? (\n <Stack spacing={0.5}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n >\n {'Add to more sites'}\n </Typography>\n <FormGroup\n sx={{ maxHeight: 200, overflowY: 'auto' }}\n >\n {orgInstall.availableHostIds.map((hostIdOption) => (\n <FormControlLabel\n key={hostIdOption}\n control={\n <Checkbox\n size=\"small\"\n checked={addHostIds.includes(\n hostIdOption,\n )}\n onChange={(event) =>\n setAddHostIds((current) =>\n event.target.checked\n ? [...current, hostIdOption]\n : current.filter(\n (id) => id !== hostIdOption,\n ),\n )\n }\n />\n }\n label={\n hosts.find(\n (host) => host.id === hostIdOption,\n )?.label ?? hostIdOption\n }\n slotProps={{\n typography: { variant: 'body2' },\n }}\n />\n ))}\n </FormGroup>\n <Box>\n <Button\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy || !addHostIds.length}\n onClick={() =>\n void runSiteEdit(() =>\n installPlan(\n listing,\n addHostIds.map((hostIdOption) => ({\n scope: 'host' as const,\n hostId: hostIdOption,\n })),\n ),\n )\n }\n >\n {'Add to selected'}\n </Button>\n </Box>\n </Stack>\n ) : null}\n\n <Divider />\n\n {/* Change the shape of the install, not just its\n membership: one org pin covers sites made\n later, which no set of host pins can. */}\n {orgInstall.orgWide ? (\n <>\n {/* The inverse of promote (AGL-1017), and the thing that makes\n per-site updates reachable at all: one org pin cannot be moved\n for one site, so a workspace that needs to stage a breaking\n version has to hold per-site pins first. Sites added later are\n no longer covered automatically — that is the trade, and the\n button says so rather than discovering it weeks later. */}\n <Button\n fullWidth\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy || !hosts.length}\n onClick={() =>\n void runSiteEdit(async () => {\n // Pin every site BEFORE dropping the org pin, so a failure\n // part-way leaves the plugin installed everywhere it already\n // was rather than nowhere.\n await installPlan(\n listing,\n hosts.map((host) => ({ scope: 'host' as const, hostId: host.id })),\n )\n await uninstall(listing, 'org')\n })\n }\n >\n {'Split into per-site installs'}\n </Button>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Pins each site separately so you can update them one at a time. ' +\n 'Sites you add later will not get this plugin automatically.'}\n </Typography>\n <Button\n fullWidth\n size=\"small\"\n color=\"inherit\"\n disabled={siteBusy}\n onClick={() =>\n setPendingUninstall({\n scope: 'org',\n targets: resolveUninstallTargets(orgInstall, 'org'),\n })\n }\n >\n {'Uninstall org-wide'}\n </Button>\n </>\n ) : (\n <Button\n fullWidth\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n disabled={siteBusy}\n onClick={() =>\n void runSiteEdit(async () => {\n // Promote: add the org pin, then drop the\n // per-site pins it makes redundant. In\n // this order, so a failure part-way\n // leaves the plugin installed everywhere\n // it already was rather than nowhere.\n await installPlan(listing, [\n { scope: 'org' },\n ])\n for (const hostIdPinned of orgInstall.hostPinnedIds) {\n await uninstall(\n listing,\n 'host',\n hostIdPinned,\n )\n }\n })\n }\n >\n {'Install for the whole organization'}\n </Button>\n )}\n\n {/* Only the two DIRECT uninstalls are confirmed. The promote and split\n flows above also call `uninstall`, but there the removal is one half of\n a move that leaves the plugin installed everywhere it already was —\n confirming \"this will stop working\" would be false. */}\n <UninstallImpactDialog\n open={pendingUninstall !== null}\n listing={listing}\n scope={pendingUninstall?.scope ?? 'host'}\n targets={pendingUninstall?.targets ?? []}\n onCancel={() => setPendingUninstall(null)}\n onConfirm={async () => {\n const target = pendingUninstall\n setPendingUninstall(null)\n if (!target) return\n await runSiteEdit(() =>\n uninstall(listing, target.scope, target.hostId),\n )\n }}\n />\n </Stack>\n )\n}\nPluginSiteSet.displayName = 'PluginSiteSet'\n\nexport default PluginSiteSet\n"],"names":["MdiIcon","mdiCheckCircle","Alert","Box","Button","Checkbox","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Divider","FormControlLabel","FormGroup","Stack","Typography","useEffect","useMemo","useState","resolveUninstallTargets","PLUGIN_ORG_WIDE_UPDATE_NOTE","pluginSiteSetSummary","pluginUpdateAll","pluginUpdatePrompt","staleSites","staleSitesOf","UninstallImpactDialog","PluginSiteSet","props","listing","hosts","orgInstall","latestVersion","installPlan","uninstall","onChanged","addHostIds","setAddHostIds","pendingUninstall","setPendingUninstall","siteBusy","setSiteBusy","sites","updateAll","String","pending","setPending","changelog","setChangelog","updatePrompt","name","displayName","label","singleSite","Boolean","hostIds","length","$id","active","fetch","encodeURIComponent","then","response","ok","json","versions","payload","entry","find","version","catch","runSiteEdit","work","installedAnywhere","spacing","direction","sx","alignItems","path","color","fontSize","mt","variant","map","site","flex","minWidth","noWrap","shadowed","size","pinnedBy","some","stale","hostId","disabled","onClick","scope","targets","orgWide","severity","target","open","onClose","maxWidth","fullWidth","title","dividers","body","others","whiteSpace","note","intent","availableHostIds","maxHeight","overflowY","hostIdOption","control","checked","includes","onChange","event","current","filter","id","host","slotProps","typography","hostIdPinned","hostPinnedIds","onCancel","onConfirm"],"mappings":"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"}
|
|
@@ -19,12 +19,7 @@ import { useUser } from "@aglyn/tenant-feature-instance";
|
|
|
19
19
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
20
20
|
import { Alert, Box, Button, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material";
|
|
21
21
|
import { useCallback, useEffect, useState } from "react";
|
|
22
|
-
|
|
23
|
-
const PLACEMENT_NOUNS = {
|
|
24
|
-
screen: 'page',
|
|
25
|
-
layout: 'layout',
|
|
26
|
-
component: 'component'
|
|
27
|
-
};
|
|
22
|
+
import { UNINSTALL_ACKNOWLEDGEMENT, uninstallImpactView } from "../model/marketplace-catalog.js";
|
|
28
23
|
/**
|
|
29
24
|
* What an uninstall breaks, before it happens (AGL-1027).
|
|
30
25
|
*
|
|
@@ -46,7 +41,7 @@ const PLACEMENT_NOUNS = {
|
|
|
46
41
|
* a dialog that implied the plugin was about to stop working would be wrong in
|
|
47
42
|
* the direction that stops people trusting it.
|
|
48
43
|
*/ export function UninstallImpactDialog(props) {
|
|
49
|
-
var _ref
|
|
44
|
+
var _ref;
|
|
50
45
|
const { open, listing, targets, scope, onCancel, onConfirm } = props;
|
|
51
46
|
const { data: user } = useUser();
|
|
52
47
|
const [impact, setImpact] = useState(null);
|
|
@@ -113,16 +108,20 @@ const PLACEMENT_NOUNS = {
|
|
|
113
108
|
}, [
|
|
114
109
|
onConfirm
|
|
115
110
|
]);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
/*
|
|
112
|
+
* The scan could not speak, so it must not be read as "nothing breaks": a
|
|
113
|
+
* failed check is a reason for MORE friction, not less — the one thing it
|
|
114
|
+
* rules out is confidence. What the dialog says, and how much friction it
|
|
115
|
+
* asks for, is `uninstallImpactView`, which the native apps replay.
|
|
116
|
+
*/ const view = uninstallImpactView({
|
|
117
|
+
name: (_ref = listing == null ? void 0 : listing.displayName) != null ? _ref : 'this plugin',
|
|
118
|
+
scope,
|
|
119
|
+
pluginId,
|
|
120
|
+
targets,
|
|
121
|
+
impact,
|
|
122
|
+
failed
|
|
123
|
+
});
|
|
124
|
+
const canConfirm = !busy && !view.scanning && (!view.needsAcknowledgement || typed.trim().toUpperCase() === UNINSTALL_ACKNOWLEDGEMENT);
|
|
126
125
|
return /*#__PURE__*/ _jsxs(Dialog, {
|
|
127
126
|
open: open,
|
|
128
127
|
onClose: ()=>!busy && onCancel(),
|
|
@@ -130,14 +129,14 @@ const PLACEMENT_NOUNS = {
|
|
|
130
129
|
fullWidth: true,
|
|
131
130
|
children: [
|
|
132
131
|
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
133
|
-
children:
|
|
132
|
+
children: view.title
|
|
134
133
|
}),
|
|
135
134
|
/*#__PURE__*/ _jsx(DialogContent, {
|
|
136
135
|
dividers: true,
|
|
137
136
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
138
137
|
spacing: 2,
|
|
139
138
|
children: [
|
|
140
|
-
scanning ? /*#__PURE__*/ _jsxs(Stack, {
|
|
139
|
+
view.scanning ? /*#__PURE__*/ _jsxs(Stack, {
|
|
141
140
|
direction: "row",
|
|
142
141
|
spacing: 1,
|
|
143
142
|
sx: {
|
|
@@ -154,24 +153,11 @@ const PLACEMENT_NOUNS = {
|
|
|
154
153
|
})
|
|
155
154
|
]
|
|
156
155
|
}) : null,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
severity: "error",
|
|
163
|
-
children: `Removes ${name} from ${(_ref2 = impact == null ? void 0 : impact.losingSites) != null ? _ref2 : losing.length} ` + `site${((_ref3 = impact == null ? void 0 : impact.losingSites) != null ? _ref3 : losing.length) === 1 ? '' : 's'}; ` + `${(impact == null ? void 0 : impact.truncated) ? 'at least ' : ''}${affected} published ` + `page${affected === 1 ? '' : 's'} ` + `place${affected === 1 ? 's' : ''} it and will stop ` + 'rendering it.'
|
|
164
|
-
}) : null,
|
|
165
|
-
!scanning && !unknown && affected === 0 && losing.length ? /*#__PURE__*/ _jsx(Alert, {
|
|
166
|
-
severity: "info",
|
|
167
|
-
children: 'No published page places this plugin, so nothing visitors can ' + 'see changes.'
|
|
168
|
-
}) : null,
|
|
169
|
-
covered.length ? // The case that would otherwise read as a lie.
|
|
170
|
-
/*#__PURE__*/ _jsx(Alert, {
|
|
171
|
-
severity: "info",
|
|
172
|
-
children: covered.length === targets.length ? 'The plugin keeps running on ' + (covered.length === 1 ? 'this site' : `all ${covered.length} of these sites`) + ', because another install still covers ' + (covered.length === 1 ? 'it' : 'them') + '. Only which version is in use changes.' : `${covered.length} of these sites keep the plugin — ` + `${covered.map((target)=>target.label).join(', ')} — because another install still covers them.`
|
|
173
|
-
}) : null,
|
|
174
|
-
impact == null ? void 0 : impact.sites.filter((site)=>!site.stillCovered && site.placements.length).map((site)=>/*#__PURE__*/ _jsxs(Box, {
|
|
156
|
+
view.alerts.map((alert)=>/*#__PURE__*/ _jsx(Alert, {
|
|
157
|
+
severity: alert.tone,
|
|
158
|
+
children: alert.text
|
|
159
|
+
}, alert.text)),
|
|
160
|
+
view.sites.map((site)=>/*#__PURE__*/ _jsxs(Box, {
|
|
175
161
|
children: [
|
|
176
162
|
/*#__PURE__*/ _jsx(Typography, {
|
|
177
163
|
variant: "subtitle2",
|
|
@@ -180,9 +166,9 @@ const PLACEMENT_NOUNS = {
|
|
|
180
166
|
/*#__PURE__*/ _jsx(Typography, {
|
|
181
167
|
variant: "caption",
|
|
182
168
|
color: "text.secondary",
|
|
183
|
-
children:
|
|
169
|
+
children: site.pages
|
|
184
170
|
}),
|
|
185
|
-
/*#__PURE__*/
|
|
171
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
186
172
|
useFlexGap: true,
|
|
187
173
|
direction: "row",
|
|
188
174
|
spacing: 1,
|
|
@@ -191,28 +177,22 @@ const PLACEMENT_NOUNS = {
|
|
|
191
177
|
gap: 1,
|
|
192
178
|
marginTop: 1
|
|
193
179
|
},
|
|
194
|
-
children:
|
|
195
|
-
site.placements.slice(0, 12).map((placement)=>/*#__PURE__*/ _jsx(Chip, {
|
|
196
|
-
size: "small",
|
|
197
|
-
variant: "outlined",
|
|
198
|
-
label: `${PLACEMENT_NOUNS[placement.type]}: ${placement.name}` + (placement.count > 1 ? ` ×${placement.count}` : '')
|
|
199
|
-
}, `${placement.type}:${placement.id}`)),
|
|
200
|
-
site.placements.length > 12 ? /*#__PURE__*/ _jsx(Chip, {
|
|
180
|
+
children: site.chips.map((chip)=>/*#__PURE__*/ _jsx(Chip, {
|
|
201
181
|
size: "small",
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
182
|
+
variant: "outlined",
|
|
183
|
+
label: chip
|
|
184
|
+
}, chip))
|
|
205
185
|
})
|
|
206
186
|
]
|
|
207
|
-
}, site.
|
|
208
|
-
|
|
187
|
+
}, site.label)),
|
|
188
|
+
view.note ? /*#__PURE__*/ _jsx(Typography, {
|
|
209
189
|
variant: "body2",
|
|
210
190
|
color: "text.secondary",
|
|
211
|
-
children:
|
|
191
|
+
children: view.note
|
|
212
192
|
}) : null,
|
|
213
|
-
needsAcknowledgement
|
|
193
|
+
view.needsAcknowledgement ? /*#__PURE__*/ _jsx(TextField, {
|
|
214
194
|
size: "small",
|
|
215
|
-
label: `Type ${
|
|
195
|
+
label: `Type ${UNINSTALL_ACKNOWLEDGEMENT} to confirm`,
|
|
216
196
|
value: typed,
|
|
217
197
|
onChange: (event)=>setTyped(event.target.value),
|
|
218
198
|
autoComplete: "off"
|
|
@@ -232,7 +212,7 @@ const PLACEMENT_NOUNS = {
|
|
|
232
212
|
variant: "contained",
|
|
233
213
|
disabled: !canConfirm,
|
|
234
214
|
onClick: ()=>void confirm(),
|
|
235
|
-
children:
|
|
215
|
+
children: view.confirmLabel
|
|
236
216
|
})
|
|
237
217
|
]
|
|
238
218
|
})
|