@aglyn/plugins-marketplace 1.18.0 → 1.20.0

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.
Files changed (44) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/editor-connections-provider.component.js +15 -0
  3. package/src/lib/components/editor-connections-provider.component.js.map +1 -1
  4. package/src/lib/components/listing-content.component.d.ts +5 -5
  5. package/src/lib/components/listing-content.component.js +87 -70
  6. package/src/lib/components/listing-content.component.js.map +1 -1
  7. package/src/lib/components/listing-detail-page.component.d.ts +2 -4
  8. package/src/lib/components/listing-detail-page.component.js.map +1 -1
  9. package/src/lib/components/marketplace-hub.component.js +10 -3
  10. package/src/lib/components/marketplace-hub.component.js.map +1 -1
  11. package/src/lib/components/plugin-site-set-panel.component.d.ts +7 -4
  12. package/src/lib/components/plugin-site-set-panel.component.js +3 -2
  13. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -1
  14. package/src/lib/components/plugin-site-set.component.js +11 -0
  15. package/src/lib/components/plugin-site-set.component.js.map +1 -1
  16. package/src/lib/constants/bundle-common.d.ts +6 -0
  17. package/src/lib/constants/bundle-common.js +5 -0
  18. package/src/lib/constants/bundle-common.js.map +1 -1
  19. package/src/lib/declarations.server.js +15 -6
  20. package/src/lib/declarations.server.js.map +1 -1
  21. package/src/lib/hooks/use-marketplace-actions.js +14 -1
  22. package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
  23. package/src/lib/model/editor-connections.d.ts +4 -3
  24. package/src/lib/model/editor-connections.js +24 -7
  25. package/src/lib/model/editor-connections.js.map +1 -1
  26. package/src/lib/model/marketplace-catalog.d.ts +25 -4
  27. package/src/lib/model/marketplace-catalog.js +31 -6
  28. package/src/lib/model/marketplace-catalog.js.map +1 -1
  29. package/src/lib/model/marketplace.d.ts +28 -2
  30. package/src/lib/model/marketplace.js +36 -0
  31. package/src/lib/model/marketplace.js.map +1 -1
  32. package/src/lib/plugin.js +8 -0
  33. package/src/lib/plugin.js.map +1 -1
  34. package/src/lib/repeat/connection-repeat-source.d.ts +55 -0
  35. package/src/lib/repeat/connection-repeat-source.js +157 -0
  36. package/src/lib/repeat/connection-repeat-source.js.map +1 -0
  37. package/src/lib/server/connections/site-records-platform.d.ts +1 -0
  38. package/src/lib/server/connections/site-records-platform.js +8 -1
  39. package/src/lib/server/connections/site-records-platform.js.map +1 -1
  40. package/src/lib/server/connections/site-records.d.ts +28 -0
  41. package/src/lib/server/connections/site-records.js +59 -1
  42. package/src/lib/server/connections/site-records.js.map +1 -1
  43. package/src/lib/server/install-plugin.js +52 -8
  44. package/src/lib/server/install-plugin.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-marketplace",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,20 +25,20 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.18.0",
29
- "@aglyn/aglyn-markdown-editor": "1.18.0",
30
- "@aglyn/shared-data-enums": "1.18.0",
31
- "@aglyn/shared-data-mdi": "1.18.0",
32
- "@aglyn/shared-ui-jsx": "1.18.0",
33
- "@aglyn/shared-ui-next": "1.18.0",
34
- "@aglyn/shared-ui-snackstack": "1.18.0",
35
- "@aglyn/shared-util-email": "1.18.0",
36
- "@aglyn/shared-util-http": "1.18.0",
37
- "@aglyn/shared-util-timestamp": "1.18.0",
38
- "@aglyn/shared-util-tools": "1.18.0",
39
- "@aglyn/tenant-data-admin": "1.18.0",
40
- "@aglyn/tenant-feature-instance": "1.18.0",
41
- "@aglyn/tenant-runtime": "1.18.0",
28
+ "@aglyn/aglyn": "1.20.0",
29
+ "@aglyn/aglyn-markdown-editor": "1.20.0",
30
+ "@aglyn/shared-data-enums": "1.20.0",
31
+ "@aglyn/shared-data-mdi": "1.20.0",
32
+ "@aglyn/shared-ui-jsx": "1.20.0",
33
+ "@aglyn/shared-ui-next": "1.20.0",
34
+ "@aglyn/shared-ui-snackstack": "1.20.0",
35
+ "@aglyn/shared-util-email": "1.20.0",
36
+ "@aglyn/shared-util-http": "1.20.0",
37
+ "@aglyn/shared-util-timestamp": "1.20.0",
38
+ "@aglyn/shared-util-tools": "1.20.0",
39
+ "@aglyn/tenant-data-admin": "1.20.0",
40
+ "@aglyn/tenant-feature-instance": "1.20.0",
41
+ "@aglyn/tenant-runtime": "1.20.0",
42
42
  "@swc/helpers": "0.5.23"
43
43
  },
44
44
  "peerDependencies": {
@@ -16,6 +16,7 @@
16
16
  */ 'use client';
17
17
  import { jsx as _jsx } from "react/jsx-runtime";
18
18
  import { EditorPluginConnectionsContext } from "@aglyn/aglyn/app-utils/plugin-connection-editor";
19
+ import { registerRepeatSource } from "@aglyn/aglyn/app-utils/repeat-sources";
19
20
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
20
21
  import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
21
22
  import { doc, getDoc } from "firebase/firestore";
@@ -23,6 +24,7 @@ import { usePathname } from "next/navigation";
23
24
  import { useEffect, useState } from "react";
24
25
  import { editorPluginConnections, syncingConnectionsOf } from "../model/editor-connections.js";
25
26
  import { PLUGIN_CONNECTIONS_API_ROUTES } from "../model/plugin-connections.js";
27
+ import { connectionRepeatSource } from "../repeat/connection-repeat-source.js";
26
28
  /** The console's editor surfaces: the Besigner and Preview of any document. */ const EDITOR_PATH = /\/(besigner|preview)(\/|$)/;
27
29
  /** A pin of `listingId` on the site, else the workspace's. */ async function readPin(firestore, hostId, orgId, listingId) {
28
30
  for (const path of [
@@ -150,6 +152,19 @@ import { PLUGIN_CONNECTIONS_API_ROUTES } from "../model/plugin-connections.js";
150
152
  firestore,
151
153
  load
152
154
  ]);
155
+ // The repeat source follows the site (AGL-4212): its picker lists this
156
+ // site's connections, and its token menu their fields. Put back to the
157
+ // site-less source when the editor closes, as the registrar left it.
158
+ useEffect(()=>{
159
+ var _value_repeatables;
160
+ if (!(value == null ? void 0 : (_value_repeatables = value.repeatables) == null ? void 0 : _value_repeatables.length)) return undefined;
161
+ registerRepeatSource(connectionRepeatSource(value));
162
+ return ()=>{
163
+ registerRepeatSource(connectionRepeatSource());
164
+ };
165
+ }, [
166
+ value
167
+ ]);
153
168
  return /*#__PURE__*/ _jsx(EditorPluginConnectionsContext.Provider, {
154
169
  value: value,
155
170
  children: children
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/editor-connections-provider.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 EditorPluginConnectionsContext,\n type EditorPluginConnections,\n} from '@aglyn/aglyn/app-utils/plugin-connection-editor'\nimport type { PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { doc, getDoc, type Firestore } from 'firebase/firestore'\nimport { usePathname } from 'next/navigation'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport {\n editorPluginConnections,\n syncingConnectionsOf,\n type EditorListingConnections,\n type EditorRealmInstall,\n} from '../model/editor-connections'\nimport { PLUGIN_CONNECTIONS_API_ROUTES } from '../model/plugin-connections'\n\n/** The console's editor surfaces: the Besigner and Preview of any document. */\nconst EDITOR_PATH = /\\/(besigner|preview)(\\/|$)/\n\nexport interface EditorConnectionsProviderProps {\n orgId?: string | null\n hostId?: string | null\n children?: ReactNode\n /** Test seam: the console path. */\n pathname?: string\n /** Test seam: reads the site's realm installs, pins and records. */\n load?: (input: { orgId: string; hostId: string }) => Promise<EditorPluginConnections | undefined>\n}\n\ntype Fetcher = (path: string) => Promise<Response>\n\n/** A pin of `listingId` on the site, else the workspace's. */\nasync function readPin(firestore: Firestore, hostId: string, orgId: string, listingId: string): Promise<unknown> {\n for (const path of [['hosts', hostId, 'installs', listingId], ['orgs', orgId, 'installs', listingId]] as const) {\n try {\n const snapshot = await getDoc(doc(firestore, path[0], path[1], path[2], path[3]))\n if (snapshot.exists()) return snapshot.data()\n } catch {\n // A pin this member cannot read is one the editor does without.\n }\n }\n return null\n}\n\n/** The record of one connection, or `null` quietly: no key, no plan, no sync yet. */\nasync function readRecord(\n fetcher: Fetcher,\n input: { hostId: string; listingId: string; connectionId: string },\n): Promise<PluginConnectionSiteRecord | null> {\n try {\n const response = await fetcher(`/api/${PLUGIN_CONNECTIONS_API_ROUTES.record}?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return null\n const answer = (await response.json().catch(() => null)) as { record?: Record<string, unknown> | null } | null\n const record = answer?.record\n if (!record || typeof record !== 'object') return null\n return {\n fields: (record['fields'] ?? {}) as PluginConnectionSiteRecord['fields'],\n items: (Array.isArray(record['items']) ? record['items'] : []) as PluginConnectionSiteRecord['items'],\n syncedAtMs: Number(record['syncedAtMs']) || 0,\n }\n } catch {\n return null\n }\n}\n\n/** Everything the editor is handed for one site, read once. */\nexport async function loadEditorPluginConnections(\n deps: { firestore: Firestore; fetcher: Fetcher },\n input: { orgId: string; hostId: string },\n): Promise<EditorPluginConnections | undefined> {\n let installs: EditorRealmInstall[]\n try {\n const response = await deps.fetcher(`/api/orgs/realm-plugins?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return undefined\n installs = (((await response.json()) as { installs?: EditorRealmInstall[] }).installs ?? []).filter(\n (install) => typeof install?.listingId === 'string' && install.listingId,\n )\n } catch {\n return undefined\n }\n const listings: EditorListingConnections[] = []\n for (const install of installs) {\n const pin = (await readPin(deps.firestore, input.hostId, input.orgId, install.listingId)) as { manifest?: unknown } | null\n const connections = syncingConnectionsOf(pin?.manifest)\n if (connections.length) listings.push({ listingId: install.listingId, connections })\n }\n if (!listings.length) return undefined\n const records: Record<string, Record<string, PluginConnectionSiteRecord>> = {}\n await Promise.all(\n listings.flatMap((listing) =>\n listing.connections.map(async (connection) => {\n const record = await readRecord(deps.fetcher, {\n hostId: input.hostId,\n listingId: listing.listingId,\n connectionId: connection.id,\n })\n if (record) (records[listing.listingId] ??= {})[connection.id] = record\n }),\n ),\n )\n return editorPluginConnections({ installs, listings, records })\n}\n\n/**\n * The site's declared connections, for the editor (AGL-4196): registered as a\n * console provider, active only on the Besigner and Preview, where the\n * canvas hands each signed element its record and the binding picker lists\n * each record's fields by name. A refusal (no site key, no plan) or a failed\n * read leaves the context empty: elements read `null`, as on a site that has\n * not connected.\n */\nexport function EditorConnectionsProvider(props: EditorConnectionsProviderProps) {\n const { orgId, hostId, children } = props\n const routerPath = usePathname()\n const pathname = props.pathname ?? routerPath ?? ''\n const firestore = useFirestore()\n const { data: user } = useUser()\n const [value, setValue] = useState<EditorPluginConnections | undefined>(undefined)\n const active = Boolean(orgId && hostId && EDITOR_PATH.test(pathname))\n const load = props.load\n const uid = (user as { uid?: string } | null | undefined)?.uid ?? ''\n\n useEffect(() => {\n if (!active || !orgId || !hostId || (!load && !uid)) {\n setValue(undefined)\n return undefined\n }\n let live = true\n const read =\n load ?? ((input) => loadEditorPluginConnections({ firestore, fetcher: (path) => authorizedFetch(user as never, path) }, input))\n void read({ orgId, hostId })\n .then((next) => {\n if (live) setValue(next)\n })\n .catch(() => {\n if (live) setValue(undefined)\n })\n return () => {\n live = false\n }\n // `user` churns with token refreshes; the member, the site and the surface name the read.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, orgId, hostId, uid, firestore, load])\n\n return <EditorPluginConnectionsContext.Provider value={value}>{children}</EditorPluginConnectionsContext.Provider>\n}\nEditorConnectionsProvider.displayName = 'EditorConnectionsProvider'\n\nexport default EditorConnectionsProvider\n"],"names":["EditorPluginConnectionsContext","authorizedFetch","useFirestore","useUser","doc","getDoc","usePathname","useEffect","useState","editorPluginConnections","syncingConnectionsOf","PLUGIN_CONNECTIONS_API_ROUTES","EDITOR_PATH","readPin","firestore","hostId","orgId","listingId","path","snapshot","exists","data","readRecord","fetcher","input","record","response","URLSearchParams","toString","ok","answer","json","catch","fields","items","Array","isArray","syncedAtMs","Number","loadEditorPluginConnections","deps","installs","undefined","filter","install","listings","pin","connections","manifest","length","push","records","Promise","all","flatMap","listing","map","connection","connectionId","id","EditorConnectionsProvider","props","children","routerPath","pathname","user","value","setValue","active","Boolean","test","load","uid","live","read","then","next","Provider","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/editor-connections-provider.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 EditorPluginConnectionsContext,\n type EditorPluginConnections,\n} from '@aglyn/aglyn/app-utils/plugin-connection-editor'\nimport type { PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { doc, getDoc, type Firestore } from 'firebase/firestore'\nimport { usePathname } from 'next/navigation'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport {\n editorPluginConnections,\n syncingConnectionsOf,\n type EditorListingConnections,\n type EditorRealmInstall,\n} from '../model/editor-connections'\nimport { PLUGIN_CONNECTIONS_API_ROUTES } from '../model/plugin-connections'\nimport { connectionRepeatSource } from '../repeat/connection-repeat-source'\n\n/** The console's editor surfaces: the Besigner and Preview of any document. */\nconst EDITOR_PATH = /\\/(besigner|preview)(\\/|$)/\n\nexport interface EditorConnectionsProviderProps {\n orgId?: string | null\n hostId?: string | null\n children?: ReactNode\n /** Test seam: the console path. */\n pathname?: string\n /** Test seam: reads the site's realm installs, pins and records. */\n load?: (input: { orgId: string; hostId: string }) => Promise<EditorPluginConnections | undefined>\n}\n\ntype Fetcher = (path: string) => Promise<Response>\n\n/** A pin of `listingId` on the site, else the workspace's. */\nasync function readPin(firestore: Firestore, hostId: string, orgId: string, listingId: string): Promise<unknown> {\n for (const path of [['hosts', hostId, 'installs', listingId], ['orgs', orgId, 'installs', listingId]] as const) {\n try {\n const snapshot = await getDoc(doc(firestore, path[0], path[1], path[2], path[3]))\n if (snapshot.exists()) return snapshot.data()\n } catch {\n // A pin this member cannot read is one the editor does without.\n }\n }\n return null\n}\n\n/** The record of one connection, or `null` quietly: no key, no plan, no sync yet. */\nasync function readRecord(\n fetcher: Fetcher,\n input: { hostId: string; listingId: string; connectionId: string },\n): Promise<PluginConnectionSiteRecord | null> {\n try {\n const response = await fetcher(`/api/${PLUGIN_CONNECTIONS_API_ROUTES.record}?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return null\n const answer = (await response.json().catch(() => null)) as { record?: Record<string, unknown> | null } | null\n const record = answer?.record\n if (!record || typeof record !== 'object') return null\n return {\n fields: (record['fields'] ?? {}) as PluginConnectionSiteRecord['fields'],\n items: (Array.isArray(record['items']) ? record['items'] : []) as PluginConnectionSiteRecord['items'],\n syncedAtMs: Number(record['syncedAtMs']) || 0,\n }\n } catch {\n return null\n }\n}\n\n/** Everything the editor is handed for one site, read once. */\nexport async function loadEditorPluginConnections(\n deps: { firestore: Firestore; fetcher: Fetcher },\n input: { orgId: string; hostId: string },\n): Promise<EditorPluginConnections | undefined> {\n let installs: EditorRealmInstall[]\n try {\n const response = await deps.fetcher(`/api/orgs/realm-plugins?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return undefined\n installs = (((await response.json()) as { installs?: EditorRealmInstall[] }).installs ?? []).filter(\n (install) => typeof install?.listingId === 'string' && install.listingId,\n )\n } catch {\n return undefined\n }\n const listings: EditorListingConnections[] = []\n for (const install of installs) {\n const pin = (await readPin(deps.firestore, input.hostId, input.orgId, install.listingId)) as { manifest?: unknown } | null\n const connections = syncingConnectionsOf(pin?.manifest)\n if (connections.length) listings.push({ listingId: install.listingId, connections })\n }\n if (!listings.length) return undefined\n const records: Record<string, Record<string, PluginConnectionSiteRecord>> = {}\n await Promise.all(\n listings.flatMap((listing) =>\n listing.connections.map(async (connection) => {\n const record = await readRecord(deps.fetcher, {\n hostId: input.hostId,\n listingId: listing.listingId,\n connectionId: connection.id,\n })\n if (record) (records[listing.listingId] ??= {})[connection.id] = record\n }),\n ),\n )\n return editorPluginConnections({ installs, listings, records })\n}\n\n/**\n * The site's declared connections, for the editor (AGL-4196): registered as a\n * console provider, active only on the Besigner and Preview, where the\n * canvas hands each signed element its record and the binding picker lists\n * each record's fields by name. A refusal (no site key, no plan) or a failed\n * read leaves the context empty: elements read `null`, as on a site that has\n * not connected.\n */\nexport function EditorConnectionsProvider(props: EditorConnectionsProviderProps) {\n const { orgId, hostId, children } = props\n const routerPath = usePathname()\n const pathname = props.pathname ?? routerPath ?? ''\n const firestore = useFirestore()\n const { data: user } = useUser()\n const [value, setValue] = useState<EditorPluginConnections | undefined>(undefined)\n const active = Boolean(orgId && hostId && EDITOR_PATH.test(pathname))\n const load = props.load\n const uid = (user as { uid?: string } | null | undefined)?.uid ?? ''\n\n useEffect(() => {\n if (!active || !orgId || !hostId || (!load && !uid)) {\n setValue(undefined)\n return undefined\n }\n let live = true\n const read =\n load ?? ((input) => loadEditorPluginConnections({ firestore, fetcher: (path) => authorizedFetch(user as never, path) }, input))\n void read({ orgId, hostId })\n .then((next) => {\n if (live) setValue(next)\n })\n .catch(() => {\n if (live) setValue(undefined)\n })\n return () => {\n live = false\n }\n // `user` churns with token refreshes; the member, the site and the surface name the read.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, orgId, hostId, uid, firestore, load])\n\n // The repeat source follows the site (AGL-4212): its picker lists this\n // site's connections, and its token menu their fields. Put back to the\n // site-less source when the editor closes, as the registrar left it.\n useEffect(() => {\n if (!value?.repeatables?.length) return undefined\n registerRepeatSource(connectionRepeatSource(value))\n return () => {\n registerRepeatSource(connectionRepeatSource())\n }\n }, [value])\n\n return <EditorPluginConnectionsContext.Provider value={value}>{children}</EditorPluginConnectionsContext.Provider>\n}\nEditorConnectionsProvider.displayName = 'EditorConnectionsProvider'\n\nexport default EditorConnectionsProvider\n"],"names":["EditorPluginConnectionsContext","registerRepeatSource","authorizedFetch","useFirestore","useUser","doc","getDoc","usePathname","useEffect","useState","editorPluginConnections","syncingConnectionsOf","PLUGIN_CONNECTIONS_API_ROUTES","connectionRepeatSource","EDITOR_PATH","readPin","firestore","hostId","orgId","listingId","path","snapshot","exists","data","readRecord","fetcher","input","record","response","URLSearchParams","toString","ok","answer","json","catch","fields","items","Array","isArray","syncedAtMs","Number","loadEditorPluginConnections","deps","installs","undefined","filter","install","listings","pin","connections","manifest","length","push","records","Promise","all","flatMap","listing","map","connection","connectionId","id","EditorConnectionsProvider","props","children","routerPath","pathname","user","value","setValue","active","Boolean","test","load","uid","live","read","then","next","repeatables","Provider","displayName"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type OrgInstallHost } from '../model/marketplace';
1
2
  export declare function ListingReadme({ readme }: {
2
3
  readme: string;
3
4
  }): import("react").JSX.Element;
@@ -41,12 +42,11 @@ export interface MarketplaceListingContentProps {
41
42
  /**
42
43
  * The org's sites, for the install-targeting picker (AGL-773). Present only
43
44
  * at org scope; when given (and non-empty) the CTA offers All sites vs
44
- * Selected sites instead of the single org/host choice.
45
+ * Selected sites instead of the single org/host choice. Each carries the
46
+ * site's own `disabledPlugins` when the shell has it (AGL-4047), which the
47
+ * installed-sites panel reads.
45
48
  */
46
- hosts?: ReadonlyArray<{
47
- id: string;
48
- label: string;
49
- }>;
49
+ hosts?: ReadonlyArray<OrgInstallHost>;
50
50
  }
51
51
  /**
52
52
  * Marketplace listing detail (AGL-95/419), relocated from the app route —
@@ -16,10 +16,12 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
- import { LISTING_README_MAX_CHARS, listingArtifactType, installsUnreviewedFallback, listingArtifactLabel, listingInclusions, hasLivePurchaseOf, hasOrgLicenceOf, licensedOrgIdsFor, installTargetsFor, isListingDeleted, isPrivateListing, resolveInstallPlan, resolveOrgInstallSummary, resolvePluginInstallState, resolveUninstallTargets, safePublisherHref, safePublisherImageSrc } from "../model/marketplace.js";
19
+ import { LISTING_README_MAX_CHARS, listingArtifactType, installsUnreviewedFallback, listingArtifactLabel, listingInclusions, hasLivePurchaseOf, hasOrgLicenceOf, licensedOrgIdsFor, installTargetsFor, isListingDeleted, isPrivateListing, resolveInstallPlan, resolveOrgInstallSummary, resolvePluginInstallState, resolveUninstallTargets, safePublisherHref, safePublisherImageSrc, allExceptInstallKind } from "../model/marketplace.js";
20
20
  import { isPublisherMemberElsewhere, landsInPublisherWorkspace, publisherMemberBuyReason } from "../model/own-listing.js";
21
21
  import { BlockedControl } from "@aglyn/shared-ui-jsx/components/blocked-control.component";
22
22
  import { PrerequisiteNotice } from "@aglyn/shared-ui-jsx/components/prerequisite-notice.component";
23
+ import { SiteSharingField } from "@aglyn/shared-ui-jsx/components/site-sharing-field.component";
24
+ import { orgPermissionLabel } from "@aglyn/aglyn/app-utils/org-permissions";
23
25
  import UninstallImpactDialog from "./uninstall-impact-dialog.component.js";
24
26
  import { ListingImage } from "./listing-image.component.js";
25
27
  import { buildRoute, parseMarkdownLite, PLATFORM_BRAND_NAME, offeredPluginVersion, PLUGIN_HOST_ABI_VERSION, pluginDocsHelp, resolveMarkdownLink, resolveMediaSrc, Route } from "@aglyn/aglyn";
@@ -27,7 +29,7 @@ import { mdiCheckCircle, mdiCheckCircleOutline, mdiEmailOutline, mdiGithub, mdiI
27
29
  import { AppLink, CardDisplay, GridItems, MdiIcon } from "@aglyn/shared-ui-jsx";
28
30
  import { NextPageTitle } from "@aglyn/shared-ui-next/contexts/next-page-title-provider";
29
31
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
30
- import { Alert, Avatar, Box, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, IconButton, Link as MuiLink, Rating, Stack, Tooltip, Typography } from "@mui/material";
32
+ import { Alert, Avatar, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton, Link as MuiLink, Rating, Stack, Tooltip, Typography } from "@mui/material";
31
33
  import { collection, doc, documentId, getDoc, limit, orderBy, query, where } from "firebase/firestore";
32
34
  import { useEffect, useMemo, useState } from "react";
33
35
  import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useFirestoreDoc, useHostOrgId, useUser, useScopeTokens } from "@aglyn/tenant-feature-instance";
@@ -427,15 +429,23 @@ export function ListingReadme({ readme }) {
427
429
  const [updateOpen, setUpdateOpen] = useState(false);
428
430
  // Screenshot lightbox (AGL-869): the clicked screenshot's URL, or null.
429
431
  const [lightboxUrl, setLightboxUrl] = useState(null);
430
- // Org-scope install targeting (AGL-773): All sites vs a chosen subset. Only
431
- // engaged at org scope with a known site list; otherwise the per-site tab's
432
- // simpler org/host choice (AGL-656) still applies.
432
+ // Org-scope install targeting (AGL-773): All sites, a chosen subset, or all
433
+ // sites except some (AGL-4047). Only engaged at org scope with a known site
434
+ // list; otherwise the per-site tab's simpler org/host choice (AGL-656) still
435
+ // applies.
433
436
  const orgTargeting = Boolean(orgScoped && hosts && hosts.length > 0);
434
- const [targeting, setTargeting] = useState('all-sites');
435
- const [selectedHostIds, setSelectedHostIds] = useState([]);
437
+ const [installTo, setInstallTo] = useState({
438
+ mode: 'all'
439
+ });
436
440
  const allHostIds = useMemo(()=>(hosts != null ? hosts : []).map((h)=>h.id), [
437
441
  hosts
438
442
  ]);
443
+ const installSites = useMemo(()=>(hosts != null ? hosts : []).map((host)=>({
444
+ id: host.id,
445
+ name: host.label
446
+ })), [
447
+ hosts
448
+ ]);
439
449
  // Listings are org-owned (AGL-652) — "did I publish this" is an org
440
450
  // comparison, resolved from the routing mirror like the browse grid.
441
451
  const [viewerOrgId, setViewerOrgId] = useState(null);
@@ -922,15 +932,49 @@ export function ListingReadme({ readme }) {
922
932
  // artifact can host-pin (plugins, components, templates, layouts) — an
923
933
  // org-only artifact can't target a subset.
924
934
  const canSelectSites = installTargets.includes('host');
935
+ // "All sites except…" (AGL-4047): a plugin pins once for the workspace and
936
+ // leaves the chosen sites out, so a site added later gets it; a per-site copy
937
+ // only picks today's sites; an org-only artifact can't leave one out.
938
+ const allExceptKind = listing ? allExceptInstallKind(listing) : null;
939
+ const installModes = useMemo(()=>[
940
+ 'all',
941
+ ...canSelectSites ? [
942
+ 'selected'
943
+ ] : [],
944
+ ...allExceptKind ? [
945
+ 'allExcept'
946
+ ] : []
947
+ ], [
948
+ canSelectSites,
949
+ allExceptKind
950
+ ]);
951
+ // A choice the artifact doesn't offer reads as All sites, never as a plan
952
+ // that silently means something else.
953
+ const installChoice = useMemo(()=>installModes.includes(installTo.mode) ? installTo : {
954
+ mode: 'all'
955
+ }, [
956
+ installModes,
957
+ installTo
958
+ ]);
959
+ const targeting = installChoice.mode === 'selected' ? 'selected-sites' : installChoice.mode === 'allExcept' ? 'all-sites-except' : 'all-sites';
925
960
  const installPlanSteps = useMemo(()=>listing ? resolveInstallPlan(listing, targeting, {
926
- selectedHostIds,
927
- allHostIds
961
+ selectedHostIds: installChoice.mode === 'selected' ? installChoice.hostIds : [],
962
+ allHostIds,
963
+ exceptHostIds: installChoice.mode === 'allExcept' ? installChoice.exceptHostIds : []
928
964
  }) : [], [
929
965
  listing,
930
966
  targeting,
931
- selectedHostIds,
967
+ installChoice,
932
968
  allHostIds
933
969
  ]);
970
+ const installModeDetails = installTargets.includes('org') ? {
971
+ all: 'Every site, including sites you add later.',
972
+ allExcept: 'Every site but the ones you leave out, including sites you add later.'
973
+ } : {
974
+ all: `Installs to all ${allHostIds.length} site` + (allHostIds.length === 1 ? '' : 's') + ". New sites won't get it automatically.",
975
+ allExcept: 'Installs to the sites you have today, but the ones you leave ' + "out. New sites won't get it automatically."
976
+ };
977
+ const installToProblem = installChoice.mode === 'selected' && !installChoice.hostIds.length ? 'Pick at least one site.' : installChoice.mode === 'allExcept' && !installChoice.exceptHostIds.length ? 'Choose at least one site to leave out, or choose All sites.' : null;
934
978
  const versionHistory = Array.isArray(listing == null ? void 0 : listing.versionHistory) ? [
935
979
  ...listing.versionHistory
936
980
  ].sort((a, b)=>b.version - a.version) : [];
@@ -940,8 +984,18 @@ export function ListingReadme({ readme }) {
940
984
  // something else.
941
985
  const installTargetSummary = ()=>{
942
986
  if (orgTargeting && !installed) {
943
- var _installPlanSteps_;
944
- if (installPlanSteps.length === 1 && ((_installPlanSteps_ = installPlanSteps[0]) == null ? void 0 : _installPlanSteps_.scope) === 'org') {
987
+ var _installPlanSteps_, _orgStep_exceptHostIds;
988
+ const orgStep = installPlanSteps.length === 1 && ((_installPlanSteps_ = installPlanSteps[0]) == null ? void 0 : _installPlanSteps_.scope) === 'org' ? installPlanSteps[0] : null;
989
+ if (orgStep == null ? void 0 : (_orgStep_exceptHostIds = orgStep.exceptHostIds) == null ? void 0 : _orgStep_exceptHostIds.length) {
990
+ // "All sites except…" (AGL-4047): the sites left out, by name.
991
+ const leftOut = orgStep.exceptHostIds.map((hostId)=>{
992
+ var _ref;
993
+ var _find;
994
+ return (_ref = (_find = (hosts != null ? hosts : []).find((host)=>host.id === hostId)) == null ? void 0 : _find.label) != null ? _ref : hostId;
995
+ });
996
+ return `every site except ${leftOut.join(', ')}, ` + 'including sites added later';
997
+ }
998
+ if (orgStep) {
945
999
  return 'the whole organization — every site, including sites added later';
946
1000
  }
947
1001
  const names = installPlanSteps.filter((step)=>step.scope === 'host').map((step)=>{
@@ -965,10 +1019,13 @@ export function ListingReadme({ readme }) {
965
1019
  // and which are still free to add. Only meaningful at org scope for a
966
1020
  // plugin — everything else installs INTO a site and has no pin to survey.
967
1021
  const orgPluginScope = Boolean(orgTargeting && isPlugin);
968
- const orgInstall = useMemo(()=>resolveOrgInstallSummary(hosts != null ? hosts : [], sitePins, orgPin != null ? orgPin : null), [
1022
+ const orgInstall = useMemo(// With each site's own switch (AGL-4047), so a site that turned the plugin
1023
+ // off, or was left out at install, is not listed as running it.
1024
+ ()=>resolveOrgInstallSummary(hosts != null ? hosts : [], sitePins, orgPin != null ? orgPin : null, listingId), [
969
1025
  hosts,
970
1026
  sitePins,
971
- orgPin
1027
+ orgPin,
1028
+ listingId
972
1029
  ]);
973
1030
  /** Run a pin write, then re-read every site's pin. */ const runSiteEdit = async (work)=>{
974
1031
  try {
@@ -1428,68 +1485,28 @@ export function ListingReadme({ readme }) {
1428
1485
  })
1429
1486
  }) : null,
1430
1487
  orgTargeting && !installed && !(orgPluginScope && orgInstall.installedAnywhere) ? /*#__PURE__*/ _jsxs(Stack, {
1431
- spacing: 1.5,
1488
+ spacing: 1,
1432
1489
  sx: {
1433
1490
  maxWidth: 420
1434
1491
  },
1435
1492
  children: [
1436
- /*#__PURE__*/ _jsxs(TextField, {
1437
- select: true,
1438
- size: "small",
1493
+ /*#__PURE__*/ _jsx(SiteSharingField, {
1439
1494
  label: "Install to",
1440
- value: targeting,
1441
- onChange: (event)=>setTargeting(event.target.value),
1442
- helperText: targeting === 'all-sites' ? installTargets.includes('org') ? 'Available on every site — including sites you add later.' : `Installs to all ${allHostIds.length} site` + (allHostIds.length === 1 ? '' : 's') + ". New sites won't get it automatically." : 'Installs only to the sites you choose.',
1443
- children: [
1444
- /*#__PURE__*/ _jsx(MenuItem, {
1445
- value: "all-sites",
1446
- children: 'All sites'
1447
- }),
1448
- canSelectSites ? /*#__PURE__*/ _jsx(MenuItem, {
1449
- value: "selected-sites",
1450
- children: 'Selected sites'
1451
- }) : null
1452
- ]
1495
+ value: installChoice,
1496
+ onChange: setInstallTo,
1497
+ sites: installSites,
1498
+ sitesReady: true,
1499
+ modes: installModes,
1500
+ modeDetails: installModeDetails,
1501
+ listLabels: {
1502
+ selected: 'Sites to install to'
1503
+ },
1504
+ problem: installToProblem
1453
1505
  }),
1454
- targeting === 'selected-sites' && canSelectSites ? /*#__PURE__*/ _jsxs(FormControl, {
1455
- component: "fieldset",
1456
- error: !selectedHostIds.length,
1457
- children: [
1458
- /*#__PURE__*/ _jsx(FormLabel, {
1459
- component: "legend",
1460
- sx: {
1461
- typography: 'caption'
1462
- },
1463
- children: 'Sites'
1464
- }),
1465
- /*#__PURE__*/ _jsx(FormGroup, {
1466
- // Scrolls rather than pushing Install off
1467
- // the page on an org with many sites.
1468
- sx: {
1469
- maxHeight: 240,
1470
- overflowY: 'auto'
1471
- },
1472
- children: (hosts != null ? hosts : []).map((host)=>/*#__PURE__*/ _jsx(FormControlLabel, {
1473
- control: /*#__PURE__*/ _jsx(Checkbox, {
1474
- size: "small",
1475
- checked: selectedHostIds.includes(host.id),
1476
- onChange: (event)=>setSelectedHostIds((current)=>event.target.checked ? [
1477
- ...current,
1478
- host.id
1479
- ] : current.filter((id)=>id !== host.id))
1480
- }),
1481
- label: host.label,
1482
- slotProps: {
1483
- typography: {
1484
- variant: 'body2'
1485
- }
1486
- }
1487
- }, host.id))
1488
- }),
1489
- /*#__PURE__*/ _jsx(FormHelperText, {
1490
- children: selectedHostIds.length ? `${selectedHostIds.length} of ${(hosts != null ? hosts : []).length} selected.` : 'Pick at least one site.'
1491
- })
1492
- ]
1506
+ installChoice.mode === 'allExcept' && allExceptKind === 'lasting' ? /*#__PURE__*/ _jsx(Typography, {
1507
+ variant: "caption",
1508
+ color: "text.secondary",
1509
+ children: 'Each site you leave out has the plugin ' + 'switched off, with the same switch as its ' + 'own Admin › Plugins page. Leaving a site ' + 'out takes ' + `“${orgPermissionLabel('site.manage')}” ` + 'there, and anyone holding it, which the ' + 'site’s admins do by default, can turn the ' + 'plugin back on for that site.'
1493
1510
  }) : null
1494
1511
  ]
1495
1512
  }) : installTargets.length > 1 && !installed ? /*#__PURE__*/ _jsxs(TextField, {