@aglyn/plugins-marketplace 1.0.0-beta.143

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 (165) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
  8. package/src/lib/components/artifact-update-dialog.component.js +244 -0
  9. package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
  10. package/src/lib/components/host-plugins-card.component.d.ts +23 -0
  11. package/src/lib/components/host-plugins-card.component.js +566 -0
  12. package/src/lib/components/host-plugins-card.component.js.map +1 -0
  13. package/src/lib/components/listing-content.component.d.ts +57 -0
  14. package/src/lib/components/listing-content.component.js +1880 -0
  15. package/src/lib/components/listing-content.component.js.map +1 -0
  16. package/src/lib/components/listing-image.component.d.ts +46 -0
  17. package/src/lib/components/listing-image.component.js +38 -0
  18. package/src/lib/components/listing-image.component.js.map +1 -0
  19. package/src/lib/components/listing-reviews.component.d.ts +22 -0
  20. package/src/lib/components/listing-reviews.component.js +419 -0
  21. package/src/lib/components/listing-reviews.component.js.map +1 -0
  22. package/src/lib/components/marketplace-browse.component.d.ts +38 -0
  23. package/src/lib/components/marketplace-browse.component.js +802 -0
  24. package/src/lib/components/marketplace-browse.component.js.map +1 -0
  25. package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
  26. package/src/lib/components/plugin-site-set-panel.component.js +137 -0
  27. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
  28. package/src/lib/components/plugin-site-set.component.d.ts +43 -0
  29. package/src/lib/components/plugin-site-set.component.js +425 -0
  30. package/src/lib/components/plugin-site-set.component.js.map +1 -0
  31. package/src/lib/components/rating-input.component.d.ts +3 -0
  32. package/src/lib/components/rating-input.component.js +47 -0
  33. package/src/lib/components/rating-input.component.js.map +1 -0
  34. package/src/lib/components/report-target.component.d.ts +36 -0
  35. package/src/lib/components/report-target.component.js +181 -0
  36. package/src/lib/components/report-target.component.js.map +1 -0
  37. package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
  38. package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
  39. package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/hooks/use-artifact-update.d.ts +41 -0
  44. package/src/lib/hooks/use-artifact-update.js +169 -0
  45. package/src/lib/hooks/use-artifact-update.js.map +1 -0
  46. package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
  47. package/src/lib/hooks/use-marketplace-actions.js +523 -0
  48. package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
  49. package/src/lib/model/artifact-merge.d.ts +154 -0
  50. package/src/lib/model/artifact-merge.js +228 -0
  51. package/src/lib/model/artifact-merge.js.map +1 -0
  52. package/src/lib/model/index.d.ts +43 -0
  53. package/src/lib/model/index.js +41 -0
  54. package/src/lib/model/index.js.map +1 -0
  55. package/src/lib/model/marketplace-props.d.ts +57 -0
  56. package/src/lib/model/marketplace-props.js +441 -0
  57. package/src/lib/model/marketplace-props.js.map +1 -0
  58. package/src/lib/model/marketplace.d.ts +833 -0
  59. package/src/lib/model/marketplace.js +929 -0
  60. package/src/lib/model/marketplace.js.map +1 -0
  61. package/src/lib/model/rating-field.d.ts +24 -0
  62. package/src/lib/model/rating-field.js +33 -0
  63. package/src/lib/model/rating-field.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +86 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server/billing-webhook.d.ts +25 -0
  68. package/src/lib/server/billing-webhook.js +1062 -0
  69. package/src/lib/server/billing-webhook.js.map +1 -0
  70. package/src/lib/server/checkout.d.ts +76 -0
  71. package/src/lib/server/checkout.js +477 -0
  72. package/src/lib/server/checkout.js.map +1 -0
  73. package/src/lib/server/connect.d.ts +24 -0
  74. package/src/lib/server/connect.js +205 -0
  75. package/src/lib/server/connect.js.map +1 -0
  76. package/src/lib/server/install-dataset-schema.d.ts +32 -0
  77. package/src/lib/server/install-dataset-schema.js +255 -0
  78. package/src/lib/server/install-dataset-schema.js.map +1 -0
  79. package/src/lib/server/install-email-starter.d.ts +65 -0
  80. package/src/lib/server/install-email-starter.js +279 -0
  81. package/src/lib/server/install-email-starter.js.map +1 -0
  82. package/src/lib/server/install-email-template.d.ts +34 -0
  83. package/src/lib/server/install-email-template.js +252 -0
  84. package/src/lib/server/install-email-template.js.map +1 -0
  85. package/src/lib/server/install-layout.d.ts +29 -0
  86. package/src/lib/server/install-layout.js +323 -0
  87. package/src/lib/server/install-layout.js.map +1 -0
  88. package/src/lib/server/install-pin-counts.d.ts +173 -0
  89. package/src/lib/server/install-pin-counts.js +248 -0
  90. package/src/lib/server/install-pin-counts.js.map +1 -0
  91. package/src/lib/server/install-plugin.d.ts +18 -0
  92. package/src/lib/server/install-plugin.js +405 -0
  93. package/src/lib/server/install-plugin.js.map +1 -0
  94. package/src/lib/server/install-template.d.ts +37 -0
  95. package/src/lib/server/install-template.js +348 -0
  96. package/src/lib/server/install-template.js.map +1 -0
  97. package/src/lib/server/install-theme.d.ts +42 -0
  98. package/src/lib/server/install-theme.js +313 -0
  99. package/src/lib/server/install-theme.js.map +1 -0
  100. package/src/lib/server/install.d.ts +27 -0
  101. package/src/lib/server/install.js +276 -0
  102. package/src/lib/server/install.js.map +1 -0
  103. package/src/lib/server/listing-versions.d.ts +26 -0
  104. package/src/lib/server/listing-versions.js +378 -0
  105. package/src/lib/server/listing-versions.js.map +1 -0
  106. package/src/lib/server/preview-image.d.ts +37 -0
  107. package/src/lib/server/preview-image.js +216 -0
  108. package/src/lib/server/preview-image.js.map +1 -0
  109. package/src/lib/server/provenance.d.ts +100 -0
  110. package/src/lib/server/provenance.js +118 -0
  111. package/src/lib/server/provenance.js.map +1 -0
  112. package/src/lib/server/publish-dataset-schema.d.ts +31 -0
  113. package/src/lib/server/publish-dataset-schema.js +202 -0
  114. package/src/lib/server/publish-dataset-schema.js.map +1 -0
  115. package/src/lib/server/publish-email-starter.d.ts +44 -0
  116. package/src/lib/server/publish-email-starter.js +231 -0
  117. package/src/lib/server/publish-email-starter.js.map +1 -0
  118. package/src/lib/server/publish-email-template.d.ts +33 -0
  119. package/src/lib/server/publish-email-template.js +211 -0
  120. package/src/lib/server/publish-email-template.js.map +1 -0
  121. package/src/lib/server/publish-layout.d.ts +33 -0
  122. package/src/lib/server/publish-layout.js +243 -0
  123. package/src/lib/server/publish-layout.js.map +1 -0
  124. package/src/lib/server/publish-plugin.d.ts +18 -0
  125. package/src/lib/server/publish-plugin.js +485 -0
  126. package/src/lib/server/publish-plugin.js.map +1 -0
  127. package/src/lib/server/publish-preconditions.d.ts +86 -0
  128. package/src/lib/server/publish-preconditions.js +160 -0
  129. package/src/lib/server/publish-preconditions.js.map +1 -0
  130. package/src/lib/server/publish-template.d.ts +27 -0
  131. package/src/lib/server/publish-template.js +215 -0
  132. package/src/lib/server/publish-template.js.map +1 -0
  133. package/src/lib/server/publish-theme.d.ts +38 -0
  134. package/src/lib/server/publish-theme.js +186 -0
  135. package/src/lib/server/publish-theme.js.map +1 -0
  136. package/src/lib/server/publish.d.ts +26 -0
  137. package/src/lib/server/publish.js +204 -0
  138. package/src/lib/server/publish.js.map +1 -0
  139. package/src/lib/server/publisher-profile-save.d.ts +30 -0
  140. package/src/lib/server/publisher-profile-save.js +226 -0
  141. package/src/lib/server/publisher-profile-save.js.map +1 -0
  142. package/src/lib/server/publisher-profile.d.ts +86 -0
  143. package/src/lib/server/publisher-profile.js +116 -0
  144. package/src/lib/server/publisher-profile.js.map +1 -0
  145. package/src/lib/server/purchase-entitlement.d.ts +83 -0
  146. package/src/lib/server/purchase-entitlement.js +118 -0
  147. package/src/lib/server/purchase-entitlement.js.map +1 -0
  148. package/src/lib/server/report.d.ts +32 -0
  149. package/src/lib/server/report.js +101 -0
  150. package/src/lib/server/report.js.map +1 -0
  151. package/src/lib/server/reviews.d.ts +41 -0
  152. package/src/lib/server/reviews.js +240 -0
  153. package/src/lib/server/reviews.js.map +1 -0
  154. package/src/lib/server/update-artifact.d.ts +19 -0
  155. package/src/lib/server/update-artifact.js +569 -0
  156. package/src/lib/server/update-artifact.js.map +1 -0
  157. package/src/lib/server/verification-request.d.ts +31 -0
  158. package/src/lib/server/verification-request.js +173 -0
  159. package/src/lib/server/verification-request.js.map +1 -0
  160. package/src/lib/server/version-stats.d.ts +202 -0
  161. package/src/lib/server/version-stats.js +305 -0
  162. package/src/lib/server/version-stats.js.map +1 -0
  163. package/src/lib/server.d.ts +30 -0
  164. package/src/lib/server.js +95 -0
  165. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,1880 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
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 } from "../model/marketplace.js";
20
+ import UninstallImpactDialog from "./uninstall-impact-dialog.component.js";
21
+ import { ListingImage } from "./listing-image.component.js";
22
+ import { buildRoute, parseMarkdownLite, PLATFORM_BRAND_NAME, offeredPluginVersion, PLUGIN_HOST_ABI_VERSION, pluginDocsHelp, resolveMarkdownLink, resolveMediaSrc, resolveUpdateState, Route, updateStateLabel } from "@aglyn/aglyn";
23
+ import { mdiCheckCircle, mdiCheckCircleOutline, mdiEmailOutline, mdiGithub, mdiInformationOutline, mdiLifebuoy, mdiLinkedin, mdiStorefrontOutline, mdiTwitter, mdiWeb } from "@aglyn/shared-data-mdi";
24
+ import { AppLink, CardDisplay, Container, GridItems, MdiIcon } from "@aglyn/shared-ui-jsx";
25
+ import { NextPageTitle } from "@aglyn/shared-ui-next/contexts/next-page-title-provider";
26
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
27
+ 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";
28
+ import { collection, doc, documentId, getDoc, limit, orderBy, query, where } from "firebase/firestore";
29
+ import { useEffect, useMemo, useState } from "react";
30
+ import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useFirestoreDoc, useHostOrgId, useUser, useScopeTokens } from "@aglyn/tenant-feature-instance";
31
+ /*
32
+ * The MODULE, not the barrel, for the two PURE helpers — the specs that render
33
+ * this page mock `@aglyn/tenant-feature-instance` wholesale to stage their
34
+ * Firestore hooks, and a query builder imported through that barrel disappears
35
+ * under the mock. Neither of these is a hook.
36
+ */ import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
37
+ import ListingReviews from "./listing-reviews.component.js";
38
+ import ReportTarget from "./report-target.component.js";
39
+ import PluginSiteSet from "./plugin-site-set.component.js";
40
+ import { MenuItem, TextField } from "@mui/material";
41
+ import { useMarketplaceActions } from "../hooks/use-marketplace-actions.js";
42
+ import { useArtifactUpdate } from "../hooks/use-artifact-update.js";
43
+ import ArtifactUpdateDialog from "./artifact-update-dialog.component.js";
44
+ /**
45
+ * How many of the site's components the page reads to answer one question.
46
+ *
47
+ * A LOOKUP ceiling, not a list: nothing here renders these documents. It only
48
+ * has to be wide enough that a component install of this listing is inside it.
49
+ */ const COMPONENT_LOOKUP_CEILING = 100;
50
+ /**
51
+ * How many of the buyer's purchase documents the page reads.
52
+ *
53
+ * Wider than the other lookups on purpose — see the query, which explains what
54
+ * a miss costs on the one panel that offers to take money.
55
+ */ const PURCHASE_LOOKUP_CEILING = 200;
56
+ /**
57
+ * How many installs OF THIS LISTING the page reads per artifact type.
58
+ *
59
+ * Narrow because the query already is: both reads carry the listing id, so
60
+ * this bounds "how many times one workspace installed one thing" rather than a
61
+ * collection.
62
+ */ const ARTIFACT_INSTALL_CEILING = 20;
63
+ const renderInlines = (inlines)=>inlines.map((inline, index)=>{
64
+ switch(inline.type){
65
+ case 'bold':
66
+ return /*#__PURE__*/ _jsx("strong", {
67
+ children: inline.text
68
+ }, index);
69
+ case 'italic':
70
+ return /*#__PURE__*/ _jsx("em", {
71
+ children: inline.text
72
+ }, index);
73
+ case 'link':
74
+ {
75
+ /*
76
+ No routing map is passed, and that is the answer rather than an
77
+ omission (AGL-3118). A link REFERENCE names a screen, listing, entry
78
+ or feed of the SITE a document was written for; a listing README is
79
+ read in the console, which serves none of them, so a reference here
80
+ resolves to nothing and renders as its words — never as an anchor
81
+ holding `entry:…`.
82
+ */ const link = resolveMarkdownLink(inline.href);
83
+ if (!('href' in link)) return /*#__PURE__*/ _jsx("span", {
84
+ children: inline.text
85
+ }, index);
86
+ return /*#__PURE__*/ _jsx(MuiLink, {
87
+ href: link.href,
88
+ target: "_blank",
89
+ rel: "noopener noreferrer",
90
+ color: "primary",
91
+ underline: "hover",
92
+ children: inline.text
93
+ }, index);
94
+ }
95
+ default:
96
+ return /*#__PURE__*/ _jsx("span", {
97
+ children: inline.text
98
+ }, index);
99
+ }
100
+ });
101
+ /**
102
+ * Publisher README (AGL-431), rendered through markdown-lite — the parser
103
+ * only ever emits text/bold/italic/http(s)-links/images, so publisher-
104
+ * written docs can't inject markup or javascript: URLs. Code blocks and
105
+ * tables carry text only (AGL-974); the same guarantee holds for both.
106
+ */ /**
107
+ * What changed between versions (AGL-976).
108
+ *
109
+ * The changelog was already collected per version and already returned by the
110
+ * versions endpoint, but the only place it appeared was a caption in the
111
+ * sidebar timeline — plain text, in a narrow column, next to a date. A buyer
112
+ * deciding whether to upgrade reads this, and publishers write it as ordinary
113
+ * markdown: a config note, a list of fixes, sometimes a fenced snippet.
114
+ *
115
+ * So it renders through the same markdown-lite path as the README, which
116
+ * gained code fences and tables in AGL-974 — and inherits the same safety
117
+ * posture: no raw HTML, `safeUrl` gating every link.
118
+ *
119
+ * APPROVED VERSIONS ONLY, and that is enforced server-side rather than here
120
+ * (AGL-976). A filter in the renderer would leave the same data leaking
121
+ * through the endpoint to anything else that reads it.
122
+ */ function ListingChangelog({ versions }) {
123
+ const entries = useMemo(()=>versions.filter((entry)=>{
124
+ var _entry_changelog;
125
+ return String((_entry_changelog = entry.changelog) != null ? _entry_changelog : '').trim();
126
+ }), [
127
+ versions
128
+ ]);
129
+ // Nothing to say is different from nothing to show: a listing whose
130
+ // publisher has never written a changelog should not get an empty card
131
+ // implying they withheld one.
132
+ if (!entries.length) return null;
133
+ return /*#__PURE__*/ _jsx(CardDisplay, {
134
+ header: 'Changelog',
135
+ help: pluginDocsHelp('publisherHandbook', {
136
+ anchor: '#versioning--updates'
137
+ }),
138
+ contentGutterX: true,
139
+ contentGutterY: true,
140
+ children: /*#__PURE__*/ _jsx(Stack, {
141
+ spacing: 2.5,
142
+ children: entries.map((entry, index)=>{
143
+ var _entry_changelog;
144
+ return /*#__PURE__*/ _jsxs(Stack, {
145
+ spacing: 1,
146
+ children: [
147
+ /*#__PURE__*/ _jsxs(Stack, {
148
+ direction: "row",
149
+ spacing: 1,
150
+ sx: {
151
+ alignItems: 'center',
152
+ flexWrap: 'wrap'
153
+ },
154
+ children: [
155
+ /*#__PURE__*/ _jsx(Typography, {
156
+ variant: "subtitle2",
157
+ children: `v${entry.version}`
158
+ }),
159
+ index === 0 ? /*#__PURE__*/ _jsx(Chip, {
160
+ size: "small",
161
+ label: "Latest"
162
+ }) : null,
163
+ entry.trust === 'realm' ? /*#__PURE__*/ _jsx(Chip, {
164
+ size: "small",
165
+ color: "success",
166
+ label: "Realm-trusted"
167
+ }) : null,
168
+ entry.publishedAtMs ? /*#__PURE__*/ _jsx(Typography, {
169
+ variant: "caption",
170
+ color: "text.secondary",
171
+ children: new Date(entry.publishedAtMs).toLocaleDateString()
172
+ }) : null
173
+ ]
174
+ }),
175
+ /*#__PURE__*/ _jsx(ListingReadme, {
176
+ readme: String((_entry_changelog = entry.changelog) != null ? _entry_changelog : '')
177
+ }),
178
+ index < entries.length - 1 ? /*#__PURE__*/ _jsx(Divider, {}) : null
179
+ ]
180
+ }, entry.version);
181
+ })
182
+ })
183
+ });
184
+ }
185
+ // Exported for the renderer-parity spec (AGL-1315).
186
+ export function ListingReadme({ readme }) {
187
+ const blocks = useMemo(()=>parseMarkdownLite(readme.slice(0, LISTING_README_MAX_CHARS)), [
188
+ readme
189
+ ]);
190
+ return /*#__PURE__*/ _jsx(Stack, {
191
+ spacing: 1.5,
192
+ children: blocks.map((block, index)=>{
193
+ switch(block.type){
194
+ case 'heading':
195
+ return /*#__PURE__*/ _jsx(Typography, {
196
+ variant: block.level === 2 ? 'h6' : 'subtitle1',
197
+ children: renderInlines(block.inlines)
198
+ }, index);
199
+ case 'image':
200
+ return /*#__PURE__*/ _jsx(Box, {
201
+ component: "img",
202
+ // Resolved like `ListingImage` next door (AGL-1686). No
203
+ // `hostId`: a listing belongs to a publishing ORG, not to a
204
+ // site, so the scope the picker baked in is already the right
205
+ // one and there is no rendering host to re-point it at.
206
+ src: resolveMediaSrc(block.src),
207
+ alt: block.alt,
208
+ loading: "lazy",
209
+ sx: {
210
+ maxWidth: '100%',
211
+ borderRadius: 1,
212
+ border: 1,
213
+ borderColor: 'divider'
214
+ }
215
+ }, index);
216
+ case 'list':
217
+ return /*#__PURE__*/ _jsx(Stack, {
218
+ component: "ul",
219
+ spacing: 0.5,
220
+ sx: {
221
+ my: 0
222
+ },
223
+ children: block.items.map((item, itemIndex)=>/*#__PURE__*/ _jsx(Typography, {
224
+ component: "li",
225
+ variant: "body2",
226
+ children: renderInlines(item)
227
+ }, itemIndex))
228
+ }, index);
229
+ // A README's install steps (AGL-1320) — a real `<ol>` carrying
230
+ // `start`, so step 4 of a continued run still reads as step 4.
231
+ case 'orderedList':
232
+ return /*#__PURE__*/ _jsx(Stack, {
233
+ component: "ol",
234
+ start: block.start,
235
+ spacing: 0.5,
236
+ sx: {
237
+ my: 0
238
+ },
239
+ children: block.items.map((item, itemIndex)=>/*#__PURE__*/ _jsx(Typography, {
240
+ component: "li",
241
+ variant: "body2",
242
+ children: renderInlines(item)
243
+ }, itemIndex))
244
+ }, index);
245
+ // A README's example snippet (AGL-974). Scrolls rather than wraps —
246
+ // a wrapped command line reads as two commands.
247
+ case 'code':
248
+ return /*#__PURE__*/ _jsx(Box, {
249
+ component: "pre",
250
+ sx: {
251
+ my: 0,
252
+ p: 1.5,
253
+ overflowX: 'auto',
254
+ borderRadius: 1,
255
+ border: 1,
256
+ borderColor: 'divider',
257
+ bgcolor: 'action.hover',
258
+ typography: 'body2',
259
+ fontFamily: 'monospace'
260
+ },
261
+ children: /*#__PURE__*/ _jsx("code", {
262
+ children: block.text
263
+ })
264
+ }, index);
265
+ // The config table (AGL-974) — the block a plugin README is
266
+ // mostly made of. Wrapped in its own scroller so a wide table
267
+ // never pushes the page sideways.
268
+ case 'table':
269
+ return /*#__PURE__*/ _jsx(Box, {
270
+ sx: {
271
+ overflowX: 'auto'
272
+ },
273
+ children: /*#__PURE__*/ _jsxs(Box, {
274
+ component: "table",
275
+ sx: {
276
+ borderCollapse: 'collapse',
277
+ width: '100%',
278
+ '& th, & td': {
279
+ border: 1,
280
+ borderColor: 'divider',
281
+ px: 1,
282
+ py: 0.5
283
+ },
284
+ '& th': {
285
+ bgcolor: 'action.hover'
286
+ }
287
+ },
288
+ children: [
289
+ /*#__PURE__*/ _jsx("thead", {
290
+ children: /*#__PURE__*/ _jsx("tr", {
291
+ children: block.header.map((cell, cellIndex)=>{
292
+ var _block_align_cellIndex;
293
+ return /*#__PURE__*/ _jsx(Typography, {
294
+ component: "th",
295
+ variant: "body2",
296
+ sx: {
297
+ textAlign: (_block_align_cellIndex = block.align[cellIndex]) != null ? _block_align_cellIndex : 'left',
298
+ fontWeight: 600
299
+ },
300
+ children: renderInlines(cell)
301
+ }, cellIndex);
302
+ })
303
+ })
304
+ }),
305
+ /*#__PURE__*/ _jsx("tbody", {
306
+ children: block.rows.map((row, rowIndex)=>/*#__PURE__*/ _jsx("tr", {
307
+ children: row.map((cell, cellIndex)=>{
308
+ var _block_align_cellIndex;
309
+ return /*#__PURE__*/ _jsx(Typography, {
310
+ component: "td",
311
+ variant: "body2",
312
+ sx: {
313
+ textAlign: (_block_align_cellIndex = block.align[cellIndex]) != null ? _block_align_cellIndex : 'left'
314
+ },
315
+ children: renderInlines(cell)
316
+ }, cellIndex);
317
+ })
318
+ }, rowIndex))
319
+ })
320
+ ]
321
+ })
322
+ }, index);
323
+ // A README quote (AGL-1315) — the surface's body size, italic
324
+ // behind a left accent in the theme's divider tone.
325
+ case 'quote':
326
+ return /*#__PURE__*/ _jsx(Typography, {
327
+ component: "blockquote",
328
+ variant: "body2",
329
+ sx: {
330
+ my: 0,
331
+ mx: 0,
332
+ pl: 1.5,
333
+ borderLeft: 3,
334
+ borderColor: 'divider',
335
+ fontStyle: 'italic',
336
+ color: 'text.secondary'
337
+ },
338
+ children: renderInlines(block.inlines)
339
+ }, index);
340
+ default:
341
+ return /*#__PURE__*/ _jsx(Typography, {
342
+ variant: "body2",
343
+ children: renderInlines(block.inlines)
344
+ }, index);
345
+ }
346
+ })
347
+ });
348
+ }
349
+ /**
350
+ * Whether the detail page has nothing to show, so the zero state stands in
351
+ * for the listing (AGL-998).
352
+ *
353
+ * Existence is `$id`, which `useFirestoreDoc`'s `idField` stamps on every
354
+ * snapshot that exists and on none that does not. A listing FIELD cannot
355
+ * answer this question: browse asks nothing about a publisher, so gating on
356
+ * `profileId` takes a listing the grid is happily showing and 404s it the
357
+ * moment someone opens it (AGL-2700). Deletion is the shared predicate, for
358
+ * the reason its own docblock gives.
359
+ *
360
+ * `!== 'loading'` and not `=== 'success'` (AGL-1066): a refused read reaches
361
+ * 'error', and a listing page rendered over `undefined` is worse than the
362
+ * zero state this exists to show.
363
+ */ export function listingIsMissing(listing, status) {
364
+ if (status === 'loading') return false;
365
+ return !(listing == null ? void 0 : listing.$id) || isListingDeleted(listing);
366
+ }
367
+ /**
368
+ * Marketplace listing detail (AGL-95/419), relocated from the app route —
369
+ * the app keeps the Dashboard chrome and renders this through the
370
+ * 'marketplaceListing' widget slot. Full description, preview image,
371
+ * version history, publisher block, and the install/buy CTA.
372
+ */ export function MarketplaceListingContent({ hostId, listingId, permissions, orgScoped, orgSlug: orgSlugProp, hosts }) {
373
+ var _ref, _ref1, _pluginState_scope, _ref2, _ref3, _ref4, _listing_ratingAverage, _listing_ratingAverage1, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12, _ref13, _ref14, _ref15, _ref16, _ref17, _ref18, _ref19;
374
+ var _componentInstall_marketplace;
375
+ // The publisher link was `/{hostDocId}/marketplace/publisher/{id}` — the
376
+ // pre-AGL-621 shape, dead since. The sibling browse grid was fixed for
377
+ // exactly this (AGL-673) and this file was missed; both now build the
378
+ // route from the shared table (AGL-685).
379
+ const { orgSlug: resolvedOrgSlug, subdomain } = useConsoleHostRoute(hostId);
380
+ // Prefer the URL slug (AGL-869): synchronous and always present at org scope.
381
+ const orgSlug = orgSlugProp != null ? orgSlugProp : resolvedOrgSlug;
382
+ const firestore = useFirestore();
383
+ const { data: user } = useUser();
384
+ const { enqueueSnackbar } = useSnackbar();
385
+ // Hoisted above the actions hook (AGL-2331): a purchase licenses an
386
+ // ORGANIZATION, so `buy` has to name the workspace it is buying for rather
387
+ // than let the server infer it from whichever site the surface happens to
388
+ // be acting through.
389
+ const orgId = useHostOrgId(hostId);
390
+ const { install, installPlan, buy, uninstall } = useMarketplaceActions(hostId, orgId);
391
+ /** The uninstall awaiting its impact check and confirmation (AGL-1027). */ const [pendingUninstall, setPendingUninstall] = useState(null);
392
+ const [installScope, setInstallScope] = useState('org');
393
+ // Confirm before writing install pins (AGL-867): the install is deliberate
394
+ // and site-scoped, so it names its targets before committing.
395
+ const [confirmOpen, setConfirmOpen] = useState(false);
396
+ // Updating a COPIED artifact is never a plain re-install (AGL-1018): the
397
+ // copy has been edited in the workspace, so the update is previewed field by
398
+ // field before anything is written.
399
+ const { preview: updatePreview, loading: updateLoading, loadPreview, applyUpdate, setPreview: setUpdatePreview } = useArtifactUpdate(hostId);
400
+ const [updateOpen, setUpdateOpen] = useState(false);
401
+ // Screenshot lightbox (AGL-869): the clicked screenshot's URL, or null.
402
+ const [lightboxUrl, setLightboxUrl] = useState(null);
403
+ // Org-scope install targeting (AGL-773): All sites vs a chosen subset. Only
404
+ // engaged at org scope with a known site list; otherwise the per-site tab's
405
+ // simpler org/host choice (AGL-656) still applies.
406
+ const orgTargeting = Boolean(orgScoped && hosts && hosts.length > 0);
407
+ const [targeting, setTargeting] = useState('all-sites');
408
+ const [selectedHostIds, setSelectedHostIds] = useState([]);
409
+ const allHostIds = useMemo(()=>(hosts != null ? hosts : []).map((h)=>h.id), [
410
+ hosts
411
+ ]);
412
+ // Listings are org-owned (AGL-652) — "did I publish this" is an org
413
+ // comparison, resolved from the routing mirror like the browse grid.
414
+ const [viewerOrgId, setViewerOrgId] = useState(null);
415
+ useEffect(()=>{
416
+ let active = true;
417
+ void getDoc(doc(firestore, 'hostIndex', hostId)).then((snapshot)=>{
418
+ var _snapshot_get;
419
+ if (active) setViewerOrgId((_snapshot_get = snapshot.get('orgId')) != null ? _snapshot_get : null);
420
+ }).catch(()=>undefined);
421
+ return ()=>{
422
+ active = false;
423
+ };
424
+ }, [
425
+ firestore,
426
+ hostId
427
+ ]);
428
+ const { data: listing, status } = useFirestoreDoc(()=>doc(firestore, 'marketplaceListings', listingId || '-missing-'), [
429
+ firestore,
430
+ listingId
431
+ ], {
432
+ idField: '$id'
433
+ });
434
+ // Targets this artifact type can actually install to (AGL-656) — only
435
+ // plugins have an org-scoped pin, so only plugins get a choice.
436
+ const installTargets = useMemo(()=>listing ? installTargetsFor(listing) : [
437
+ 'host'
438
+ ], [
439
+ listing
440
+ ]);
441
+ const { data: profile } = useFirestoreDoc(()=>{
442
+ var _ref;
443
+ return doc(firestore, 'publisherProfiles', (_ref = listing == null ? void 0 : listing.profileId) != null ? _ref : '-anonymous-');
444
+ }, [
445
+ firestore,
446
+ listing == null ? void 0 : listing.profileId
447
+ ], {
448
+ idField: '$id'
449
+ });
450
+ /*
451
+ * ORDERED AND CEILINGED (AGL-2501). The page folds this into ONE answer —
452
+ * is a component install of THIS listing present — so a bare `limit`, which
453
+ * Firestore answers in document-id order, would let an arbitrary hundred of
454
+ * the site's components decide it. A site past such a window reads as
455
+ * not-installed, and the button below then offers to install it again.
456
+ */ const { data: installedRead } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'components'), COMPONENT_LOOKUP_CEILING), [
457
+ firestore,
458
+ hostId
459
+ ], {
460
+ idField: '$id'
461
+ });
462
+ const { rows: installedDocs, truncated: installedTruncated } = useMemo(()=>ceilingedWindow(installedRead, COMPONENT_LOOKUP_CEILING), [
463
+ installedRead
464
+ ]);
465
+ // Held at null until the uid resolves (AGL-1440): `marketplacePurchases`
466
+ // is buyer/seller-gated and a LIST is evaluated against the QUERY, so the
467
+ // `-anonymous-` sentinel was denied wholesale on every mount and retried
468
+ // on the refusal cadence. A signed-out viewer simply has no purchases.
469
+ /*
470
+ * ORDERED AND CEILINGED, and the ceiling stays wide (AGL-2501).
471
+ *
472
+ * `orderBy(documentId())` costs nothing here: an equality `where` plus
473
+ * `orderBy(__name__)` is served by the automatic single-field index on
474
+ * `buyerUid`, so no composite index stands between this page and a buyer's
475
+ * licences. Ordering on `purchasedAt` would need one, and would also DROP
476
+ * any purchase written without the field.
477
+ *
478
+ * The window is wider than the others because of what a miss costs here.
479
+ * Every other lookup on this page decides a chip; this one decides whether
480
+ * the Buy button appears, so a purchase that falls outside it does not
481
+ * merely under-report state — it offers to sell a licence the buyer already
482
+ * holds. The probe row is what lets the panel say so.
483
+ */ const { data: purchaseRead } = useFirestoreCollection(()=>(user == null ? void 0 : user.uid) ? query(collection(firestore, 'marketplacePurchases'), where('buyerUid', '==', user.uid), orderBy(documentId()), limit(PURCHASE_LOOKUP_CEILING + 1)) : null, [
484
+ firestore,
485
+ user == null ? void 0 : user.uid
486
+ ], {
487
+ idField: '$id'
488
+ });
489
+ const { rows: purchaseDocs, truncated: purchasesTruncated } = useMemo(()=>ceilingedWindow(purchaseRead, PURCHASE_LOOKUP_CEILING), [
490
+ purchaseRead
491
+ ]);
492
+ const artifactType = listing ? listingArtifactType(listing) : null;
493
+ const isPlugin = artifactType === 'plugin';
494
+ // Plugin installs are version PINS, not component snapshots (AGL-656): a
495
+ // host pin lives at `hosts/{h}/installs/{id}`, an org pin at
496
+ // `orgs/{o}/installs/{id}` and applies to every site. Reading only
497
+ // `components` above never sees either, so an installed plugin read as
498
+ // "not installed" here; the two pins tell the honest story, host over org.
499
+ // (`orgId` is resolved above, where `buy` needs it.)
500
+ const { data: hostPin } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'installs', listingId || '-missing-'), [
501
+ firestore,
502
+ hostId,
503
+ listingId
504
+ ], {
505
+ idField: '$id'
506
+ });
507
+ // Held at null while `useHostOrgId` is in flight, never `orgs/-pending-`
508
+ // (AGL-1440): installs are member-gated, so the sentinel was a
509
+ // guaranteed-denied listen on every listing view.
510
+ const { data: orgPin } = useFirestoreDoc(()=>orgId && listingId ? doc(firestore, 'orgs', orgId, 'installs', listingId) : null, [
511
+ firestore,
512
+ orgId,
513
+ listingId
514
+ ], {
515
+ idField: '$id'
516
+ });
517
+ // Every site's pin, not just the acting one (AGL-997). At org scope the
518
+ // question is "which of my sites is this on", and the two subscriptions
519
+ // above can only answer for one arbitrary site. Read as a fan-in rather
520
+ // than a subscription per host: the host count is a prop, so a per-host
521
+ // hook would change the hook count between renders, and a collection-group
522
+ // query would need an index plus a rules audit to read other orgs' pins
523
+ // it must never see. `pinsNonce` re-reads after this page's own writes,
524
+ // which are the only thing that changes these docs while it is open.
525
+ const [sitePins, setSitePins] = useState({});
526
+ const [pinsNonce, setPinsNonce] = useState(0);
527
+ const hostIdsKey = (hosts != null ? hosts : []).map((host)=>host.id).join('|');
528
+ useEffect(()=>{
529
+ if (!orgScoped || !listingId || !(hosts == null ? void 0 : hosts.length)) return;
530
+ let active = true;
531
+ void Promise.all(hosts.map(async (host)=>{
532
+ const snapshot = await getDoc(doc(firestore, 'hosts', host.id, 'installs', listingId));
533
+ return [
534
+ host.id,
535
+ snapshot.exists() ? snapshot.data() : null
536
+ ];
537
+ })).then((entries)=>{
538
+ if (active) setSitePins(Object.fromEntries(entries));
539
+ }).catch(()=>undefined);
540
+ return ()=>{
541
+ active = false;
542
+ };
543
+ // eslint-disable-next-line react-hooks/exhaustive-deps
544
+ }, [
545
+ firestore,
546
+ listingId,
547
+ hostIdsKey,
548
+ orgScoped,
549
+ pinsNonce
550
+ ]);
551
+ // datasetSchema/emailTemplate installs live in neither collection above
552
+ // (AGL-789) — they become an org dataset or a draft email version, each
553
+ // stamped with the listing it came from. Scoped to this listing rather than
554
+ // reading the whole collection, since the detail page only speaks for one.
555
+ // Scoped (AGL-1044): the AGL-1041 rules reject an unfiltered list for a
556
+ // scoped member, so this would error rather than show fewer installs.
557
+ //
558
+ // Wait for the member doc before listing (AGL-1047). `useScopeTokens`
559
+ // reports `orgWide: true` while loading, so without `scopeLoaded` a scoped
560
+ // collaborator's first render computes `needsScope: false` and sends an
561
+ // UNFILTERED list that the AGL-1041 rules deny per document. It recovers
562
+ // on the next render, which is what makes it easy to miss: the page looks
563
+ // right and logs a denial every mount.
564
+ const { tokens: scopeTokens, orgWide: viewerOrgWide, loaded: scopeLoaded } = useScopeTokens(orgId != null ? orgId : undefined);
565
+ const needsScope = Boolean(orgId) && !viewerOrgWide;
566
+ /*
567
+ * ORDERED (AGL-2501). These two are already narrowed to ONE listing, so the
568
+ * twenty is generous rather than tight — but `artifactInstall` below takes
569
+ * the FIRST live row out of whatever comes back, and an unordered `limit` is
570
+ * answered in document-id order, which makes "the first" an arbitrary one.
571
+ * On an org holding more than twenty installs of a single listing the window
572
+ * could hold only deleted rows while a live one sat outside it, and the page
573
+ * would offer to install what is already installed.
574
+ *
575
+ * `documentId()` is what the existing composite index on
576
+ * (`source.listingId`, `visibleTo`) already orders by — Firestore appends the
577
+ * document name to every index — so this needs no new one.
578
+ */ const { data: datasetInstalls } = useFirestoreCollection(()=>orgId && scopeLoaded ? query(collection(firestore, 'orgs', orgId, 'datasets'), where('source.listingId', '==', listingId || '-missing-'), ...needsScope ? [
579
+ where('visibleTo', 'array-contains-any', scopeTokens)
580
+ ] : [], orderBy(documentId()), limit(ARTIFACT_INSTALL_CEILING)) : null, [
581
+ firestore,
582
+ orgId,
583
+ scopeLoaded,
584
+ listingId,
585
+ needsScope,
586
+ scopeTokens
587
+ ], // Same mutable-predicate exposure as the browse datasets query:
588
+ // `visibleTo` is rewritten by every scope edit, and the rule requires the
589
+ // constraint for anyone who is not org-wide (AGL-1196).
590
+ {
591
+ idField: '$id',
592
+ confirmDisappearances: true
593
+ });
594
+ const { data: emailInstalls } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'emailTemplates'), where('installedFrom.listingId', '==', listingId || '-missing-'), orderBy(documentId()), limit(ARTIFACT_INSTALL_CEILING)), [
595
+ firestore,
596
+ hostId,
597
+ listingId
598
+ ], {
599
+ idField: '$id'
600
+ });
601
+ // A theme installs onto the host document itself (AGL-1020) rather than into
602
+ // a collection, so there is no per-listing query to scope — the site either
603
+ // runs this listing's theme or it does not.
604
+ const { data: themeHostDoc } = useFirestoreDoc(()=>artifactType === 'theme' ? doc(firestore, 'hosts', hostId) : null, [
605
+ firestore,
606
+ hostId,
607
+ artifactType
608
+ ], {
609
+ idField: '$id'
610
+ });
611
+ const artifactInstall = useMemo(()=>{
612
+ if (artifactType === 'datasetSchema') {
613
+ var _ref;
614
+ var _hit_source;
615
+ const hit = (datasetInstalls != null ? datasetInstalls : []).find((entry)=>!entry.deletedAt);
616
+ return hit ? {
617
+ version: (_ref = (_hit_source = hit.source) == null ? void 0 : _hit_source.version) != null ? _ref : null
618
+ } : undefined;
619
+ }
620
+ if (artifactType === 'emailTemplate') {
621
+ var _ref1;
622
+ var _hit_installedFrom;
623
+ const hit = (emailInstalls != null ? emailInstalls : []).find((entry)=>!entry.deletedAt);
624
+ return hit ? {
625
+ version: (_ref1 = (_hit_installedFrom = hit.installedFrom) == null ? void 0 : _hit_installedFrom.version) != null ? _ref1 : null
626
+ } : undefined;
627
+ }
628
+ if (artifactType === 'theme') {
629
+ var _ref2;
630
+ const stamp = themeHostDoc == null ? void 0 : themeHostDoc.themeInstalledFrom;
631
+ return (stamp == null ? void 0 : stamp.listingId) === listingId ? {
632
+ version: (_ref2 = stamp == null ? void 0 : stamp.version) != null ? _ref2 : null
633
+ } : undefined;
634
+ }
635
+ return undefined;
636
+ }, [
637
+ artifactType,
638
+ datasetInstalls,
639
+ emailInstalls,
640
+ themeHostDoc,
641
+ listingId
642
+ ]);
643
+ /**
644
+ * The kill switch, on the page with the Install button (AGL-2368).
645
+ *
646
+ * One document read for the whole page — the switch is listing-scoped —
647
+ * and public, which is what lets the marketplace loaders and the
648
+ * host-plugins card read it already.
649
+ */ const { data: revocation } = useFirestoreDoc(()=>isPlugin && listingId ? doc(firestore, 'revocations', listingId) : null, [
650
+ firestore,
651
+ isPlugin,
652
+ listingId
653
+ ]);
654
+ /**
655
+ * The version this listing actually offers (AGL-1016, AGL-2368).
656
+ *
657
+ * For a plugin that is the newest INSTALLABLE version, not `latestVersion`
658
+ * and not merely the newest approved one. This page used to compare against
659
+ * `latestVersion`, so publishing v1.1.0 lit up "Update to v1.1.0" for every
660
+ * workspace the moment it was uploaded — and the install route then refused
661
+ * it, because AGL-966 only ever hands out reviewed bytes. The badge was
662
+ * advertising what the marketplace would not give you.
663
+ *
664
+ * `latestApprovedVersion` fixed that and left the same defect one state
665
+ * over: approval is a review verdict and revocation does not clear it, so a
666
+ * killed version stayed the offer. This is the page with the Install and
667
+ * Buy buttons on it, so it cross-checks the mirror against the switch
668
+ * rather than trusting a cache two writes behind.
669
+ */ const offeredVersion = isPlugin ? offeredPluginVersion(listing, revocation) : listing == null ? void 0 : listing.latestVersion;
670
+ /**
671
+ * The publisher installing their OWN unreviewed bytes (AGL-1083).
672
+ *
673
+ * `install-plugin` deliberately allows it — you cannot test a version you
674
+ * cannot install, and the AGL-969 checklist asks a publisher to confirm
675
+ * they tested these exact bytes on a site they control. But the affordance
676
+ * said nothing, so it read exactly like installing reviewed, signed code.
677
+ *
678
+ * The condition mirrors the route's fallback rather than approximating it:
679
+ * there, `newest ?? fallback` hands over `latestVersion` ONLY when the
680
+ * owner asks and no approved version exists at all. With an approved
681
+ * version present the server serves that one, and this is not an
682
+ * unreviewed install however new the pending one is.
683
+ */ const unreviewedInstall = isPlugin && installsUnreviewedFallback(listing, viewerOrgId, revocation);
684
+ /** What would actually land — the route's fallback, named. */ const installingVersion = unreviewedInstall ? listing == null ? void 0 : listing.latestVersion : offeredVersion;
685
+ const pluginState = useMemo(()=>resolvePluginInstallState(offeredVersion, isPlugin ? hostPin : null, isPlugin ? orgPin : null), [
686
+ offeredVersion,
687
+ isPlugin,
688
+ hostPin,
689
+ orgPin
690
+ ]);
691
+ // Public version history + trust tier (AGL-431): the pluginVersions
692
+ // docs are server-only, so the marketplace API exposes the buyer-safe
693
+ // subset (version/changelog/trust/hostAbi/date).
694
+ const [versions, setVersions] = useState([]);
695
+ const [totals, setTotals] = useState(null);
696
+ useEffect(()=>{
697
+ // Every artifact type now, not just plugins (AGL-1036): the route serves
698
+ // the per-version install counts for all of them, and a component's
699
+ // publisher wants "who is still on v1" as much as a plugin's does.
700
+ if (!listing || !listingId) return;
701
+ let active = true;
702
+ void fetch(`/api/marketplace/listing-versions?listingId=${encodeURIComponent(listingId)}`).then((response)=>response.ok ? response.json() : {
703
+ versions: []
704
+ }).then((payload)=>{
705
+ var _ref, _payload_installCount, _payload_activeInstalls, _payload_untrackedActiveInstalls, _payload_untrackedInstallCount;
706
+ if (!active) return;
707
+ setVersions((_ref = payload == null ? void 0 : payload.versions) != null ? _ref : []);
708
+ setTotals(typeof (payload == null ? void 0 : payload.activeInstalls) === 'number' ? {
709
+ installCount: Number((_payload_installCount = payload.installCount) != null ? _payload_installCount : 0),
710
+ activeInstalls: Number((_payload_activeInstalls = payload.activeInstalls) != null ? _payload_activeInstalls : 0),
711
+ untrackedActiveInstalls: Number((_payload_untrackedActiveInstalls = payload.untrackedActiveInstalls) != null ? _payload_untrackedActiveInstalls : 0),
712
+ // THIS PROJECTION is where the field died (AGL-2339). The
713
+ // route served it from both branches; the client copied its
714
+ // sibling and not it, so the page could never render what the
715
+ // server had already computed.
716
+ untrackedInstallCount: Number((_payload_untrackedInstallCount = payload.untrackedInstallCount) != null ? _payload_untrackedInstallCount : 0)
717
+ } : null);
718
+ }).catch(()=>undefined);
719
+ return ()=>{
720
+ active = false;
721
+ };
722
+ // Keyed on the listing's id, not its legacy `type` (AGL-864): an
723
+ // artifactType-only plugin has no `type`, so keying on it left the effect
724
+ // stuck at its undefined first value and the versions fetch never fired.
725
+ //
726
+ // `pinsNonce` re-reads after this page's own writes, so the per-version
727
+ // install counts (AGL-1036) are not left showing the tally from before the
728
+ // update the user just pressed.
729
+ }, [
730
+ listing == null ? void 0 : listing.$id,
731
+ listingId,
732
+ pinsNonce
733
+ ]);
734
+ /**
735
+ * The install numbers this page is allowed to print (AGL-1418).
736
+ *
737
+ * The route's reconciled totals when they have arrived, and the listing
738
+ * document's own copy only until then. That fallback cannot reintroduce the
739
+ * contradiction it replaced: the version history it used to disagree with is
740
+ * fed by the same response, so before it lands there is nothing on screen to
741
+ * disagree with.
742
+ */ const installTotals = useMemo(()=>{
743
+ var _ref, _ref1;
744
+ if (totals) return totals;
745
+ const stored = Number((_ref = listing == null ? void 0 : listing.activeInstalls) != null ? _ref : Number.NaN);
746
+ if (!Number.isFinite(stored)) return null;
747
+ return {
748
+ installCount: Number((_ref1 = listing == null ? void 0 : listing.installCount) != null ? _ref1 : 0),
749
+ activeInstalls: stored,
750
+ // Zero on the fallback, because the listing document holds no split at
751
+ // all — claiming a shortfall it cannot know would be the contradiction
752
+ // this fallback exists to avoid.
753
+ untrackedActiveInstalls: 0,
754
+ untrackedInstallCount: 0
755
+ };
756
+ }, [
757
+ totals,
758
+ listing == null ? void 0 : listing.activeInstalls,
759
+ listing == null ? void 0 : listing.installCount
760
+ ]);
761
+ const latestEntry = versions[0];
762
+ const realmTrusted = versions.some((entry)=>entry.trust === 'realm');
763
+ const abiIncompatible = (latestEntry == null ? void 0 : latestEntry.hostAbi) != null && latestEntry.hostAbi !== PLUGIN_HOST_ABI_VERSION;
764
+ // Component listings still install into `hosts/{h}/components`; plugin
765
+ // state comes from the pins above. `installed` unifies the two so the CTA
766
+ // and the buy gate read the same for either artifact type.
767
+ const componentInstall = useMemo(()=>(installedDocs != null ? installedDocs : []).find((definition)=>{
768
+ var _definition_marketplace;
769
+ return (definition == null ? void 0 : (_definition_marketplace = definition.marketplace) == null ? void 0 : _definition_marketplace.listingId) === listingId && !definition.deletedAt;
770
+ }), [
771
+ installedDocs,
772
+ listingId
773
+ ]);
774
+ // The SERVER's predicate, imported rather than restated (AGL-2158). This
775
+ // used to be `some(p => p.listingId === listingId)` with no refund test,
776
+ // while `hasLivePurchase` — the gate on all eight ways into paid content —
777
+ // treats a purchase carrying `refundedAt` as absent. The one buyer the two
778
+ // must agree about is the refunded one, and they disagreed: the page said
779
+ // "Purchased" and hid the buy button, the install routes answered 402, and
780
+ // the buyer could neither install nor buy it again.
781
+ //
782
+ // ORG-SCOPED SINCE AGL-2331. `hasLivePurchaseOf` answers "did this PERSON
783
+ // buy it", which is no longer the question either the install routes or the
784
+ // checkout guard ask: a licence belongs to a workspace, so the acting org is
785
+ // what decides whether this page may install. Reading the person's answer
786
+ // here would put the CTA back in the disagreement AGL-2158 closed — an
787
+ // agency owner would see "Purchased" in a client workspace that holds no
788
+ // licence, and the install would 402.
789
+ const purchased = useMemo(()=>hasOrgLicenceOf(purchaseDocs != null ? purchaseDocs : [], listingId, {
790
+ orgId,
791
+ uid: user == null ? void 0 : user.uid
792
+ }), [
793
+ purchaseDocs,
794
+ listingId,
795
+ orgId,
796
+ user == null ? void 0 : user.uid
797
+ ]);
798
+ /**
799
+ * The buyer holds a licence for this listing — in some OTHER workspace
800
+ * (AGL-2331).
801
+ *
802
+ * The state that has no honest rendering without it: an agency developer who
803
+ * bought this component for one client opens it in the next client's
804
+ * workspace and sees a plain Buy button, with nothing to distinguish "you
805
+ * have never bought this" from "you own one, it just does not cover the
806
+ * workspace you are standing in". Both are a purchase — but only one of them
807
+ * is a surprise when the invoice arrives.
808
+ *
809
+ * Reads the buyer's OWN purchase documents, which the rules have always let
810
+ * them read, so this needs no cross-org read to answer.
811
+ */ const licensedElsewhere = useMemo(()=>!purchased && hasLivePurchaseOf(purchaseDocs != null ? purchaseDocs : [], listingId) && licensedOrgIdsFor(purchaseDocs != null ? purchaseDocs : [], listingId).some((licensed)=>licensed !== orgId), [
812
+ purchased,
813
+ purchaseDocs,
814
+ listingId,
815
+ orgId
816
+ ]);
817
+ const missing = listingIsMissing(listing, status);
818
+ // Somewhere to go from the zero state (AGL-998). Same surface split as
819
+ // every other link here: org route at org scope, host route otherwise,
820
+ // and undefined rather than a link to nowhere when the slug is unresolved.
821
+ const marketplaceHref = orgScoped ? orgSlug ? buildRoute(Route.ORG_MARKETPLACE, {
822
+ orgSlug
823
+ }) : undefined : // so both branches now land on the same org-scope page and the
824
+ // subdomain is no longer part of the URL.
825
+ orgSlug ? buildRoute(Route.ORG_MARKETPLACE, {
826
+ orgSlug
827
+ }) : undefined;
828
+ const installed = isPlugin ? pluginState.scope != null : Boolean(componentInstall != null ? componentInstall : artifactInstall);
829
+ const installedVersion = isPlugin ? pluginState.installedVersion : (_ref = componentInstall == null ? void 0 : (_componentInstall_marketplace = componentInstall.marketplace) == null ? void 0 : _componentInstall_marketplace.version) != null ? _ref : artifactInstall == null ? void 0 : artifactInstall.version;
830
+ const upToDate = isPlugin ? installed && !pluginState.updateAvailable : componentInstall && installedVersion >= (listing == null ? void 0 : listing.latestVersion);
831
+ /**
832
+ * The same update comparison the Plugins index and the installation page
833
+ * make (AGL-1016), so all three agree on the version pair rather than each
834
+ * re-deriving it. Only rendered when the viewer already has it installed —
835
+ * "you have v1.0.0 · v1.1.0 available" is meaningless to a first-time buyer.
836
+ */ const updateStatus = useMemo(()=>resolveUpdateState(isPlugin ? hostPin != null ? hostPin : orgPin : // collections here, each with its own field for the version, so
837
+ // they are normalised to the one shape provenance reads. The
838
+ // `installedFrom` stamp wins when AGL-1015 wrote one.
839
+ _extends({}, (componentInstall == null ? void 0 : componentInstall.installedFrom) ? {
840
+ installedFrom: componentInstall.installedFrom
841
+ } : {}, installedVersion != null && listingId ? {
842
+ listingId,
843
+ version: installedVersion
844
+ } : {}), listing != null ? listing : null, artifactType, // The kill switch (AGL-2368) — this line said "Update to vX" for a
845
+ // version the install route refuses, because approval survives it.
846
+ revocation), // eslint-disable-next-line react-hooks/exhaustive-deps
847
+ [
848
+ isPlugin,
849
+ hostPin,
850
+ orgPin,
851
+ componentInstall,
852
+ installedVersion,
853
+ listingId,
854
+ listing,
855
+ artifactType,
856
+ revocation
857
+ ]);
858
+ const priceUsd = Number((_ref1 = listing == null ? void 0 : listing.priceUsd) != null ? _ref1 : 0);
859
+ const mustBuy = priceUsd > 0 && !purchased && (listing == null ? void 0 : listing.profileId) !== viewerOrgId && !installed;
860
+ // Updating an installed plugin stays at its current scope; a fresh install
861
+ // uses the picker (plugins) or the artifact's only target (everything else).
862
+ const installTargetScope = isPlugin && installed ? (_pluginState_scope = pluginState.scope) != null ? _pluginState_scope : undefined : installTargets.length > 1 ? installScope : undefined;
863
+ // Org-scope targeting (AGL-773): "Selected sites" is only offered when the
864
+ // artifact can host-pin (plugins, components, templates, layouts) — an
865
+ // org-only artifact can't target a subset.
866
+ const canSelectSites = installTargets.includes('host');
867
+ const installPlanSteps = useMemo(()=>listing ? resolveInstallPlan(listing, targeting, {
868
+ selectedHostIds,
869
+ allHostIds
870
+ }) : [], [
871
+ listing,
872
+ targeting,
873
+ selectedHostIds,
874
+ allHostIds
875
+ ]);
876
+ const versionHistory = Array.isArray(listing == null ? void 0 : listing.versionHistory) ? [
877
+ ...listing.versionHistory
878
+ ].sort((a, b)=>b.version - a.version) : [];
879
+ // Human summary of exactly where this install lands (AGL-867), read from the
880
+ // same targeting the CTA acts on. An org pin covers every site (now and
881
+ // future); host pins are named so "Selected sites" can't silently mean
882
+ // something else.
883
+ const installTargetSummary = ()=>{
884
+ if (orgTargeting && !installed) {
885
+ var _installPlanSteps_;
886
+ if (installPlanSteps.length === 1 && ((_installPlanSteps_ = installPlanSteps[0]) == null ? void 0 : _installPlanSteps_.scope) === 'org') {
887
+ return 'the whole organization — every site, including sites added later';
888
+ }
889
+ const names = installPlanSteps.filter((step)=>step.scope === 'host').map((step)=>{
890
+ var _ref;
891
+ var _find;
892
+ return (_ref = (_find = (hosts != null ? hosts : []).find((host)=>host.id === step.hostId)) == null ? void 0 : _find.label) != null ? _ref : step.hostId;
893
+ });
894
+ if (names.length) {
895
+ return `${names.length} site${names.length === 1 ? '' : 's'}: ${names.join(', ')}`;
896
+ }
897
+ return 'the selected targets';
898
+ }
899
+ // Org-only artifacts (dataset schema) always land org-wide — including on
900
+ // re-add, where installTargetScope isn't set (AGL-867).
901
+ if (installTargetScope === 'org' || installTargets.length === 1 && installTargets[0] === 'org') {
902
+ return 'the whole organization — every site';
903
+ }
904
+ return 'this site';
905
+ };
906
+ // The org-wide picture (AGL-997): which sites run this, at which version,
907
+ // and which are still free to add. Only meaningful at org scope for a
908
+ // plugin — everything else installs INTO a site and has no pin to survey.
909
+ const orgPluginScope = Boolean(orgTargeting && isPlugin);
910
+ const orgInstall = useMemo(()=>resolveOrgInstallSummary(hosts != null ? hosts : [], sitePins, orgPin != null ? orgPin : null), [
911
+ hosts,
912
+ sitePins,
913
+ orgPin
914
+ ]);
915
+ /** Run a pin write, then re-read every site's pin. */ const runSiteEdit = async (work)=>{
916
+ try {
917
+ await work();
918
+ } finally{
919
+ setPinsNonce((current)=>current + 1);
920
+ }
921
+ };
922
+ /**
923
+ * A copied artifact with a newer version on offer (AGL-1018).
924
+ *
925
+ * The distinction that matters: a plugin takes an update by moving a pointer,
926
+ * but a component, layout or dataset schema was copied in and then EDITED
927
+ * here, so "Update" is a merge. It goes through the review dialog, never
928
+ * straight to the install route.
929
+ */ //
930
+ // Every non-current state, not just `update-available` (AGL-1034): a copy
931
+ // whose versions cannot be ordered, or that reports `ahead`, is still a copy
932
+ // this workspace may have edited, and sending it to the install route would
933
+ // dead-end on the divergence 409 with nothing offering the review it names.
934
+ const copiedUpdatable = !isPlugin && installed && // A theme is a copied artifact like a component, just stored as a field on
935
+ // the site rather than as its own document (AGL-1020) — so it takes the
936
+ // same diff-first update path, and must not fall through to the plain
937
+ // install that would overwrite the site's edits.
938
+ Boolean(componentInstall || artifactType === 'theme' && artifactInstall) && updateStatus.state !== 'current';
939
+ const openUpdateReview = ()=>{
940
+ setUpdatePreview(null);
941
+ setUpdateOpen(true);
942
+ if (listingId) void loadPreview(listingId);
943
+ };
944
+ /**
945
+ * "Install as a new copy", which means different things per type.
946
+ *
947
+ * Only a component or layout has a link that can be DETACHED, leaving the
948
+ * customised copy in place as an ordinary artifact — the update route does
949
+ * that. For the others a fresh copy is exactly what the ordinary install
950
+ * already produces (another empty dataset, another inactive draft, a replaced
951
+ * template bundle), quota checks included, so it goes there rather than
952
+ * through a second implementation that would drift from it.
953
+ */ const runCopy = async ()=>{
954
+ if (artifactType === 'component' || artifactType === 'layout') {
955
+ return void await runUpdate('copy');
956
+ }
957
+ setUpdateOpen(false);
958
+ await runSiteEdit(()=>install(listing, installTargetScope, {
959
+ onDiverged: openUpdateReview
960
+ }));
961
+ };
962
+ const runUpdate = async (mode, takePaths = [], confirmDestructive = false)=>{
963
+ if (!listingId) return;
964
+ const done = await applyUpdate(listingId, {
965
+ mode,
966
+ takePaths,
967
+ confirmDestructive
968
+ });
969
+ // Left open on failure — a destructive schema update comes back asking for
970
+ // the confirmation tick, and closing would hide the question.
971
+ if (done) {
972
+ setUpdateOpen(false);
973
+ // Re-read the install state and the per-version counts this just moved.
974
+ setPinsNonce((current)=>current + 1);
975
+ }
976
+ };
977
+ // The actual pin write, run only after the confirm dialog (AGL-867).
978
+ const runInstall = ()=>{
979
+ setConfirmOpen(false);
980
+ // Re-read every site's pin afterwards (AGL-997). The acting host's pin is
981
+ // a live subscription, but the other sites' are a fan-in read — without
982
+ // this the page went straight from "Install" to a bare disabled
983
+ // "Installed" button, because the panel that explains WHERE it landed
984
+ // had not seen the write yet.
985
+ void runSiteEdit(()=>orgTargeting && !installed ? installPlan(listing, installPlanSteps) : // instead of toasting advice nothing here acts on (AGL-1034).
986
+ install(listing, installTargetScope, {
987
+ onDiverged: openUpdateReview
988
+ }));
989
+ };
990
+ return /*#__PURE__*/ _jsxs(_Fragment, {
991
+ children: [
992
+ /*#__PURE__*/ _jsx(NextPageTitle, {
993
+ screen: (_ref2 = listing == null ? void 0 : listing.displayName) != null ? _ref2 : 'Marketplace listing'
994
+ }),
995
+ /*#__PURE__*/ _jsxs(Container, {
996
+ gutterY: true,
997
+ maxWidth: "xl",
998
+ children: [
999
+ missing ? /*#__PURE__*/ _jsx(CardDisplay, {
1000
+ contentGutterX: true,
1001
+ contentGutterY: true,
1002
+ children: /*#__PURE__*/ _jsxs(Stack, {
1003
+ spacing: 1.5,
1004
+ sx: {
1005
+ alignItems: 'center',
1006
+ textAlign: 'center',
1007
+ py: 6
1008
+ },
1009
+ children: [
1010
+ /*#__PURE__*/ _jsx(MdiIcon, {
1011
+ path: mdiStorefrontOutline.path,
1012
+ color: "disabled",
1013
+ sx: {
1014
+ fontSize: 48
1015
+ }
1016
+ }),
1017
+ /*#__PURE__*/ _jsx(Typography, {
1018
+ variant: "h6",
1019
+ children: 'This listing isn’t here'
1020
+ }),
1021
+ /*#__PURE__*/ _jsx(Typography, {
1022
+ variant: "body2",
1023
+ color: "text.secondary",
1024
+ sx: {
1025
+ maxWidth: 440
1026
+ },
1027
+ children: 'It may have been unpublished by its publisher, taken ' + 'down, or the link may point somewhere that no longer ' + 'exists. Anything you already installed from it keeps ' + 'working.'
1028
+ }),
1029
+ marketplaceHref ? /*#__PURE__*/ _jsx(AppLink, {
1030
+ href: marketplaceHref,
1031
+ children: /*#__PURE__*/ _jsx(Button, {
1032
+ variant: "contained",
1033
+ color: "primary",
1034
+ component: "span",
1035
+ children: 'Back to the marketplace'
1036
+ })
1037
+ }) : null
1038
+ ]
1039
+ })
1040
+ }) : /*#__PURE__*/ _jsx(GridItems, {
1041
+ spacing: 3,
1042
+ // Masonry. Source order here is body (8),
1043
+ // changelog (8), sidebar (4) — and two eights cannot share a
1044
+ // twelve-column row, so the changelog wrapped and dragged the
1045
+ // sidebar down with it. `Install`, the point of the page, opened
1046
+ // below the fold under a screen of blank space. Masonry places
1047
+ // the sidebar in the columns beside the body card where it
1048
+ // belongs, and no card can be pushed past the container's edge.
1049
+ masonry: true,
1050
+ items: [
1051
+ {
1052
+ size: {
1053
+ xs: 12,
1054
+ md: 8
1055
+ },
1056
+ // No card header (AGL-1000): the page's hero now carries the
1057
+ // listing's name, and the only route that renders this
1058
+ // widget is the org listing page — the per-site one
1059
+ // redirects to it (AGL-775) — so a heading here would just
1060
+ // print the same words twice, inches apart.
1061
+ children: /*#__PURE__*/ _jsx(CardDisplay, {
1062
+ contentGutterX: true,
1063
+ contentGutterY: true,
1064
+ children: /*#__PURE__*/ _jsxs(Stack, {
1065
+ spacing: 2,
1066
+ children: [
1067
+ /*#__PURE__*/ _jsx(ListingImage, {
1068
+ src: listing == null ? void 0 : listing.previewImageUrl,
1069
+ alt: `${listing == null ? void 0 : listing.displayName} preview`,
1070
+ sx: {
1071
+ width: '100%',
1072
+ maxHeight: 360,
1073
+ objectFit: 'cover',
1074
+ borderRadius: 1,
1075
+ border: 1,
1076
+ borderColor: 'divider'
1077
+ }
1078
+ }),
1079
+ /*#__PURE__*/ _jsxs(Stack, {
1080
+ direction: "row",
1081
+ spacing: 1,
1082
+ sx: {
1083
+ alignItems: 'center',
1084
+ flexWrap: 'wrap'
1085
+ },
1086
+ children: [
1087
+ (listing == null ? void 0 : listing.logoUrl) ? /*#__PURE__*/ _jsx(Box, {
1088
+ component: "img",
1089
+ src: listing.logoUrl,
1090
+ alt: `${listing == null ? void 0 : listing.displayName} logo`,
1091
+ sx: {
1092
+ width: 32,
1093
+ height: 32,
1094
+ borderRadius: 1,
1095
+ objectFit: 'cover'
1096
+ }
1097
+ }) : null,
1098
+ listing ? /*#__PURE__*/ _jsx(Chip, {
1099
+ size: "small",
1100
+ color: "primary",
1101
+ label: listingArtifactLabel(listing)
1102
+ }) : null,
1103
+ ((_ref3 = listing == null ? void 0 : listing.categories) != null ? _ref3 : []).map((entry)=>/*#__PURE__*/ _jsx(Chip, {
1104
+ size: "small",
1105
+ label: entry
1106
+ }, entry)),
1107
+ (listing == null ? void 0 : listing.category) && !((_ref4 = listing == null ? void 0 : listing.categories) != null ? _ref4 : []).length ? /*#__PURE__*/ _jsx(Chip, {
1108
+ size: "small",
1109
+ label: listing.category
1110
+ }) : null,
1111
+ (listing == null ? void 0 : listing.license) ? /*#__PURE__*/ _jsx(Chip, {
1112
+ size: "small",
1113
+ variant: "outlined",
1114
+ label: listing.license
1115
+ }) : null,
1116
+ isPlugin ? realmTrusted ? /*#__PURE__*/ _jsx(Chip, {
1117
+ size: "small",
1118
+ color: "success",
1119
+ label: "Realm-trusted"
1120
+ }) : /*#__PURE__*/ _jsx(Chip, {
1121
+ size: "small",
1122
+ variant: "outlined",
1123
+ label: "Sandboxed"
1124
+ }) : null,
1125
+ (listing == null ? void 0 : listing.reviewStatus) === 'verified' ? /*#__PURE__*/ _jsx(Tooltip, {
1126
+ title: `A human at ${PLATFORM_BRAND_NAME} confirmed ` + 'who this ' + 'publisher is, and that the listing describes ' + 'what the code does. A claim about the ' + 'publisher, not this release — it survives a ' + 'version bump.',
1127
+ children: /*#__PURE__*/ _jsx(Chip, {
1128
+ size: "small",
1129
+ color: "info",
1130
+ label: "Verified publisher"
1131
+ })
1132
+ }) : null,
1133
+ (listing == null ? void 0 : listing.latestVersionReviewState) === 'approved' ? /*#__PURE__*/ _jsx(Tooltip, {
1134
+ title: `A human at ${PLATFORM_BRAND_NAME} read these ` + 'exact bytes — ' + 'the version on offer — against a required ' + 'checklist. Re-earned per version. Not a ' + 'security guarantee: every plugin runs in the ' + 'same sandbox either way.',
1135
+ children: /*#__PURE__*/ _jsx(Chip, {
1136
+ size: "small",
1137
+ color: "success",
1138
+ label: "Reviewed"
1139
+ })
1140
+ }) : null,
1141
+ isPrivateListing(listing != null ? listing : {}) ? /*#__PURE__*/ _jsx(Chip, {
1142
+ size: "small",
1143
+ variant: "outlined",
1144
+ label: "Private"
1145
+ }) : null,
1146
+ /*#__PURE__*/ _jsx(Chip, {
1147
+ size: "small",
1148
+ color: priceUsd > 0 ? 'primary' : 'default',
1149
+ label: priceUsd > 0 ? `$${priceUsd}` : 'Free'
1150
+ }),
1151
+ (listing == null ? void 0 : listing.ratingCount) ? /*#__PURE__*/ _jsxs(Stack, {
1152
+ direction: "row",
1153
+ spacing: 0.5,
1154
+ sx: {
1155
+ alignItems: 'center'
1156
+ },
1157
+ children: [
1158
+ /*#__PURE__*/ _jsx(Rating, {
1159
+ size: "small",
1160
+ readOnly: true,
1161
+ precision: 0.1,
1162
+ value: Number((_listing_ratingAverage = listing.ratingAverage) != null ? _listing_ratingAverage : 0)
1163
+ }),
1164
+ /*#__PURE__*/ _jsx(Typography, {
1165
+ variant: "caption",
1166
+ color: "text.secondary",
1167
+ children: `${(_listing_ratingAverage1 = listing.ratingAverage) != null ? _listing_ratingAverage1 : 0} (${listing.ratingCount})`
1168
+ })
1169
+ ]
1170
+ }) : null,
1171
+ /*#__PURE__*/ _jsx(Tooltip, {
1172
+ title: 'Counted once per install, whether it covers a ' + 'single site or a whole organization — and ' + 'including the publisher’s own. “All time” ' + 'counts installs that have since been removed, ' + 'so it never falls.',
1173
+ children: /*#__PURE__*/ _jsxs(Typography, {
1174
+ variant: "caption",
1175
+ color: "text.secondary",
1176
+ children: [
1177
+ `v${(_ref5 = listing == null ? void 0 : listing.latestVersion) != null ? _ref5 : '…'}`,
1178
+ (installTotals == null ? void 0 : installTotals.installCount) || (installTotals == null ? void 0 : installTotals.activeInstalls) ? ` · ${installTotals.activeInstalls} active ` + `install${installTotals.activeInstalls === 1 ? '' : 's'}` : '',
1179
+ (installTotals == null ? void 0 : installTotals.installCount) ? ` · ${installTotals.installCount} all time` : ''
1180
+ ]
1181
+ })
1182
+ })
1183
+ ]
1184
+ }),
1185
+ /*#__PURE__*/ _jsx(Typography, {
1186
+ variant: "body2",
1187
+ color: "text.secondary",
1188
+ children: (_ref6 = listing == null ? void 0 : listing.description) != null ? _ref6 : 'No description provided.'
1189
+ }),
1190
+ isPlugin && !realmTrusted && (listing == null ? void 0 : listing.latestVersionReviewState) !== 'approved' ? /*#__PURE__*/ _jsx(Alert, {
1191
+ severity: "info",
1192
+ children: 'Marketplace plugin: runs sandboxed and cannot ' + 'access your site data directly. Review the ' + 'publisher and docs before installing.'
1193
+ }) : null,
1194
+ isPrivateListing(listing != null ? listing : {}) ? /*#__PURE__*/ _jsx(Alert, {
1195
+ severity: "info",
1196
+ children: 'Private plugin: it is never listed in the ' + 'marketplace, and only your organization’s ' + 'sites can install it. It passes the same ' + 'review as every other plugin.'
1197
+ }) : null,
1198
+ abiIncompatible ? /*#__PURE__*/ _jsx(Alert, {
1199
+ severity: "warning",
1200
+ children: `Built for platform generation ${latestEntry == null ? void 0 : latestEntry.hostAbi}; ` + `this platform runs ${PLUGIN_HOST_ABI_VERSION}. ` + 'It will not load until the publisher ships a ' + 'compatible version.'
1201
+ }) : null,
1202
+ ((_ref7 = listing == null ? void 0 : listing.screenshots) != null ? _ref7 : []).length ? /*#__PURE__*/ _jsx(Stack, {
1203
+ direction: "row",
1204
+ spacing: 1,
1205
+ sx: {
1206
+ overflowX: 'auto',
1207
+ pb: 0.5
1208
+ },
1209
+ children: listing.screenshots.map((url)=>// Click to zoom (AGL-869) — the strip thumbnails
1210
+ // are too small to actually evaluate a UI from.
1211
+ /*#__PURE__*/ _jsx(Box, {
1212
+ component: "img",
1213
+ src: url,
1214
+ alt: `${listing == null ? void 0 : listing.displayName} screenshot`,
1215
+ loading: "lazy",
1216
+ onClick: ()=>setLightboxUrl(url),
1217
+ sx: {
1218
+ height: 180,
1219
+ borderRadius: 1,
1220
+ border: 1,
1221
+ borderColor: 'divider',
1222
+ cursor: 'zoom-in'
1223
+ }
1224
+ }, url))
1225
+ }) : null,
1226
+ (listing == null ? void 0 : listing.readme) ? /*#__PURE__*/ _jsxs(_Fragment, {
1227
+ children: [
1228
+ /*#__PURE__*/ _jsx(Divider, {}),
1229
+ /*#__PURE__*/ _jsx(ListingReadme, {
1230
+ readme: listing.readme
1231
+ })
1232
+ ]
1233
+ }) : null
1234
+ ]
1235
+ })
1236
+ })
1237
+ },
1238
+ {
1239
+ size: {
1240
+ xs: 12,
1241
+ md: 8
1242
+ },
1243
+ children: /*#__PURE__*/ _jsx(ListingChangelog, {
1244
+ versions: versions
1245
+ })
1246
+ },
1247
+ {
1248
+ size: {
1249
+ xs: 12,
1250
+ md: 4
1251
+ },
1252
+ children: /*#__PURE__*/ _jsxs(Stack, {
1253
+ spacing: 3,
1254
+ children: [
1255
+ /*#__PURE__*/ _jsx(CardDisplay, {
1256
+ header: 'Install',
1257
+ help: pluginDocsHelp('installYourFirstPlugin', {
1258
+ anchor: '#step-4-targeting'
1259
+ }),
1260
+ contentGutterX: true,
1261
+ contentGutterY: true,
1262
+ children: /*#__PURE__*/ _jsxs(Stack, {
1263
+ spacing: 1.5,
1264
+ children: [
1265
+ /*#__PURE__*/ _jsxs(Stack, {
1266
+ direction: "row",
1267
+ spacing: 1,
1268
+ sx: {
1269
+ alignItems: 'baseline'
1270
+ },
1271
+ children: [
1272
+ /*#__PURE__*/ _jsx(Typography, {
1273
+ variant: "h6",
1274
+ children: priceUsd > 0 ? `$${priceUsd}` : 'Free'
1275
+ }),
1276
+ /*#__PURE__*/ _jsx(Typography, {
1277
+ variant: "caption",
1278
+ color: "text.secondary",
1279
+ children: `v${(_ref8 = listing == null ? void 0 : listing.latestVersion) != null ? _ref8 : '…'}`
1280
+ })
1281
+ ]
1282
+ }),
1283
+ /*#__PURE__*/ _jsx(Divider, {}),
1284
+ /*#__PURE__*/ _jsxs(Stack, {
1285
+ spacing: 0.75,
1286
+ children: [
1287
+ /*#__PURE__*/ _jsx(Typography, {
1288
+ variant: "overline",
1289
+ color: "text.secondary",
1290
+ children: "What's included"
1291
+ }),
1292
+ listingInclusions(listing != null ? listing : {}, {
1293
+ reviewedVersion: (listing == null ? void 0 : listing.latestVersionReviewState) === 'approved'
1294
+ }).map((row)=>/*#__PURE__*/ _jsxs(Stack, {
1295
+ direction: "row",
1296
+ spacing: 1,
1297
+ sx: {
1298
+ alignItems: 'flex-start'
1299
+ },
1300
+ children: [
1301
+ /*#__PURE__*/ _jsx(MdiIcon, {
1302
+ path: row.tone === 'included' ? mdiCheckCircleOutline : mdiInformationOutline,
1303
+ size: 0.8,
1304
+ color: row.tone === 'included' ? 'success' : 'disabled'
1305
+ }),
1306
+ /*#__PURE__*/ _jsx(Typography, {
1307
+ variant: "body2",
1308
+ children: row.label
1309
+ })
1310
+ ]
1311
+ }, row.label))
1312
+ ]
1313
+ }),
1314
+ /*#__PURE__*/ _jsx(Divider, {}),
1315
+ unreviewedInstall ? /*#__PURE__*/ _jsxs(Alert, {
1316
+ severity: "warning",
1317
+ variant: "outlined",
1318
+ children: [
1319
+ /*#__PURE__*/ _jsx(Typography, {
1320
+ variant: "body2",
1321
+ children: `No reviewer has approved v${listing == null ? void 0 : listing.latestVersion} yet.`
1322
+ }),
1323
+ /*#__PURE__*/ _jsx(Typography, {
1324
+ variant: "caption",
1325
+ component: "div",
1326
+ children: 'You can install it because you publish it — ' + 'that is how you test a version before ' + 'submitting it. Nobody else can install it ' + 'until it is approved.'
1327
+ })
1328
+ ]
1329
+ }) : null,
1330
+ installed && updateStatus.state !== 'current' && updateStatus.installedVersion ? /*#__PURE__*/ _jsx(Alert, {
1331
+ severity: updateStatus.state === 'update-available' ? 'info' : 'warning',
1332
+ variant: "outlined",
1333
+ children: updateStateLabel(updateStatus)
1334
+ }) : null,
1335
+ orgPluginScope ? /*#__PURE__*/ _jsx(PluginSiteSet, {
1336
+ listing: listing,
1337
+ hosts: hosts != null ? hosts : [],
1338
+ orgInstall: orgInstall,
1339
+ latestVersion: offeredVersion,
1340
+ installPlan: installPlan,
1341
+ uninstall: uninstall,
1342
+ onChanged: ()=>setPinsNonce((current)=>current + 1)
1343
+ }) : null,
1344
+ !orgPluginScope && isPlugin && installed ? /*#__PURE__*/ _jsx(Stack, {
1345
+ spacing: 1,
1346
+ children: /*#__PURE__*/ _jsxs(Stack, {
1347
+ direction: "row",
1348
+ spacing: 1,
1349
+ sx: {
1350
+ alignItems: 'flex-start'
1351
+ },
1352
+ children: [
1353
+ /*#__PURE__*/ _jsx(MdiIcon, {
1354
+ path: mdiCheckCircle.path,
1355
+ color: "success",
1356
+ sx: {
1357
+ fontSize: 20,
1358
+ mt: '2px'
1359
+ }
1360
+ }),
1361
+ /*#__PURE__*/ _jsxs(Typography, {
1362
+ variant: "body2",
1363
+ children: [
1364
+ pluginState.shadowed ? `Installed on this site (v${installedVersion}), ` + 'overriding the organization-wide install.' : pluginState.scope === 'org' ? `Installed for the whole organization ` + `(v${installedVersion}) — available on every site.` : `Installed on this site (v${installedVersion}).`,
1365
+ pluginState.updateAvailable ? ` A newer version (v${offeredVersion}) is available.` : ''
1366
+ ]
1367
+ })
1368
+ ]
1369
+ })
1370
+ }) : null,
1371
+ orgTargeting && !installed && !(orgPluginScope && orgInstall.installedAnywhere) ? /*#__PURE__*/ _jsxs(Stack, {
1372
+ spacing: 1.5,
1373
+ sx: {
1374
+ maxWidth: 420
1375
+ },
1376
+ children: [
1377
+ /*#__PURE__*/ _jsxs(TextField, {
1378
+ select: true,
1379
+ size: "small",
1380
+ label: "Install to",
1381
+ value: targeting,
1382
+ onChange: (event)=>setTargeting(event.target.value),
1383
+ 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.',
1384
+ children: [
1385
+ /*#__PURE__*/ _jsx(MenuItem, {
1386
+ value: "all-sites",
1387
+ children: 'All sites'
1388
+ }),
1389
+ canSelectSites ? /*#__PURE__*/ _jsx(MenuItem, {
1390
+ value: "selected-sites",
1391
+ children: 'Selected sites'
1392
+ }) : null
1393
+ ]
1394
+ }),
1395
+ targeting === 'selected-sites' && canSelectSites ? /*#__PURE__*/ _jsxs(FormControl, {
1396
+ component: "fieldset",
1397
+ error: !selectedHostIds.length,
1398
+ children: [
1399
+ /*#__PURE__*/ _jsx(FormLabel, {
1400
+ component: "legend",
1401
+ sx: {
1402
+ typography: 'caption'
1403
+ },
1404
+ children: 'Sites'
1405
+ }),
1406
+ /*#__PURE__*/ _jsx(FormGroup, {
1407
+ // Scrolls rather than pushing Install off
1408
+ // the page on an org with many sites.
1409
+ sx: {
1410
+ maxHeight: 240,
1411
+ overflowY: 'auto'
1412
+ },
1413
+ children: (hosts != null ? hosts : []).map((host)=>/*#__PURE__*/ _jsx(FormControlLabel, {
1414
+ control: /*#__PURE__*/ _jsx(Checkbox, {
1415
+ size: "small",
1416
+ checked: selectedHostIds.includes(host.id),
1417
+ onChange: (event)=>setSelectedHostIds((current)=>event.target.checked ? [
1418
+ ...current,
1419
+ host.id
1420
+ ] : current.filter((id)=>id !== host.id))
1421
+ }),
1422
+ label: host.label,
1423
+ slotProps: {
1424
+ typography: {
1425
+ variant: 'body2'
1426
+ }
1427
+ }
1428
+ }, host.id))
1429
+ }),
1430
+ /*#__PURE__*/ _jsx(FormHelperText, {
1431
+ children: selectedHostIds.length ? `${selectedHostIds.length} of ${(hosts != null ? hosts : []).length} selected.` : 'Pick at least one site.'
1432
+ })
1433
+ ]
1434
+ }) : null
1435
+ ]
1436
+ }) : installTargets.length > 1 && !installed ? /*#__PURE__*/ _jsxs(TextField, {
1437
+ select: true,
1438
+ size: "small",
1439
+ label: "Install to",
1440
+ value: installScope,
1441
+ onChange: (event)=>setInstallScope(event.target.value),
1442
+ helperText: installScope === 'org' ? 'Available to every site in this organization. A site can still override it for itself.' : 'This site only.',
1443
+ sx: {
1444
+ maxWidth: 360
1445
+ },
1446
+ children: [
1447
+ /*#__PURE__*/ _jsx(MenuItem, {
1448
+ value: "org",
1449
+ children: 'This organization — all sites'
1450
+ }),
1451
+ /*#__PURE__*/ _jsx(MenuItem, {
1452
+ value: "host",
1453
+ children: 'This site only'
1454
+ })
1455
+ ]
1456
+ }) : null,
1457
+ mustBuy && licensedElsewhere ? /*#__PURE__*/ _jsx(Alert, {
1458
+ severity: "info",
1459
+ sx: {
1460
+ mb: 1
1461
+ },
1462
+ children: 'You already own this in another workspace. A ' + 'license covers one organization, so this one ' + 'needs its own.'
1463
+ }) : null,
1464
+ purchasesTruncated || installedTruncated ? /*#__PURE__*/ _jsx(Alert, {
1465
+ severity: "warning",
1466
+ sx: {
1467
+ mb: 1
1468
+ },
1469
+ children: (purchasesTruncated ? `Checked against the first ${PURCHASE_LOOKUP_CEILING} of your purchases, ordered by id; you have more. ` : `Checked against the first ${COMPONENT_LOOKUP_CEILING} components on this site, ordered by id; there are more. `) + 'If you already own or installed this, it may ' + 'not be reflected here — check your receipts ' + 'before buying again.'
1470
+ }) : null,
1471
+ /*#__PURE__*/ _jsx(Box, {
1472
+ sx: {
1473
+ display: orgPluginScope && orgInstall.installedAnywhere ? 'none' : undefined
1474
+ },
1475
+ children: /*#__PURE__*/ _jsx(Button, {
1476
+ // Full-width and contained (AGL-1005): in a
1477
+ // sidebar card this is the page's primary action,
1478
+ // and "already installed and current" is the only
1479
+ // state where it steps back to an outline.
1480
+ fullWidth: true,
1481
+ variant: upToDate ? 'outlined' : 'contained',
1482
+ color: "primary",
1483
+ disabled: Boolean(upToDate) || !(listing == null ? void 0 : listing.profileId) || // Nothing selected to install to (AGL-773).
1484
+ orgTargeting && !installed && !mustBuy && installPlanSteps.length === 0,
1485
+ onClick: permissions.installPlugins ? ()=>// Buying goes straight to Stripe checkout,
1486
+ // which is its own confirmation; a free or
1487
+ // entitled install confirms its targets
1488
+ // first (AGL-867). Updating a copied
1489
+ // artifact reviews the diff before writing
1490
+ // anything (AGL-1018).
1491
+ mustBuy ? buy(listing) : copiedUpdatable ? openUpdateReview() : setConfirmOpen(true) : ()=>enqueueSnackbar('Your team role does not allow installing from the marketplace', {
1492
+ variant: 'warning',
1493
+ persist: false
1494
+ }),
1495
+ children: upToDate ? `Installed (v${installedVersion})` : copiedUpdatable ? // update would do and writes nothing. Without
1496
+ // an ordered version pair there is no "to
1497
+ // vN" to promise (AGL-1034), so it says the
1498
+ // honest, smaller thing.
1499
+ updateStatus.state === 'update-available' ? `Review update to v${offeredVersion}` : 'Review this version' : artifactInstall ? // draft, so this stays enabled (AGL-789). A
1500
+ // theme has no "again" — there is one per
1501
+ // site, and re-pressing this re-applies it.
1502
+ artifactType === 'theme' ? `Applied (v${installedVersion}) · re-apply` : `${artifactType === 'emailTemplate' ? 'Draft added' : 'Added'} (v${installedVersion}) · add again` : installed ? `Update to v${offeredVersion}` : mustBuy ? licensedElsewhere ? `Buy for this workspace — $${priceUsd}` : `Buy for $${priceUsd}` : unreviewedInstall ? `Install unreviewed v${listing == null ? void 0 : listing.latestVersion} for testing` : orgTargeting || installTargets.length > 1 ? 'Install' : 'Add to this site'
1503
+ })
1504
+ }),
1505
+ !orgPluginScope && isPlugin && installed ? /*#__PURE__*/ _jsx(Button, {
1506
+ fullWidth: true,
1507
+ size: "small",
1508
+ color: "inherit",
1509
+ onClick: ()=>setPendingUninstall({
1510
+ scope: pluginState.scope === 'org' ? 'org' : 'host',
1511
+ targets: resolveUninstallTargets(orgInstall, pluginState.scope === 'org' ? 'org' : 'host', hostId)
1512
+ }),
1513
+ children: pluginState.scope === 'org' ? 'Uninstall org-wide' : 'Uninstall'
1514
+ }) : null
1515
+ ]
1516
+ })
1517
+ }),
1518
+ /*#__PURE__*/ _jsx(CardDisplay, {
1519
+ header: 'Publisher',
1520
+ help: pluginDocsHelp('publisherHandbook', {
1521
+ anchor: '#before-your-first-publish'
1522
+ }),
1523
+ contentGutterX: true,
1524
+ contentGutterY: true,
1525
+ children: /*#__PURE__*/ _jsxs(Stack, {
1526
+ spacing: 0.5,
1527
+ children: [
1528
+ /*#__PURE__*/ _jsx(Avatar, {
1529
+ src: safePublisherHref(profile == null ? void 0 : profile.avatarUrl),
1530
+ alt: (_ref9 = profile == null ? void 0 : profile.displayName) != null ? _ref9 : 'Publisher',
1531
+ variant: "rounded",
1532
+ sx: {
1533
+ width: 40,
1534
+ height: 40,
1535
+ mb: 0.5
1536
+ },
1537
+ children: String((_ref10 = (_ref11 = profile == null ? void 0 : profile.displayName) != null ? _ref11 : profile == null ? void 0 : profile.handle) != null ? _ref10 : '?').slice(0, 1).toUpperCase()
1538
+ }),
1539
+ /*#__PURE__*/ _jsx(MuiLink, {
1540
+ href: !(listing == null ? void 0 : listing.profileId) ? undefined : orgScoped ? orgSlug ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {
1541
+ orgSlug,
1542
+ // Handle, not id (AGL-1001); the id
1543
+ // still resolves for the beat
1544
+ // before the profile loads.
1545
+ handle: (_ref12 = profile == null ? void 0 : profile.handle) != null ? _ref12 : listing.profileId
1546
+ }) : undefined : orgSlug && subdomain ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {
1547
+ orgSlug,
1548
+ handle: (_ref13 = profile == null ? void 0 : profile.handle) != null ? _ref13 : listing.profileId
1549
+ }) : undefined,
1550
+ color: "primary",
1551
+ underline: "hover",
1552
+ variant: "body2",
1553
+ children: (_ref14 = profile == null ? void 0 : profile.displayName) != null ? _ref14 : (profile == null ? void 0 : profile.handle) ? `@${profile.handle}` : '…'
1554
+ }),
1555
+ (profile == null ? void 0 : profile.handle) ? /*#__PURE__*/ _jsx(Typography, {
1556
+ variant: "caption",
1557
+ color: "text.secondary",
1558
+ children: `@${profile.handle}`
1559
+ }) : null,
1560
+ (profile == null ? void 0 : profile.bio) ? /*#__PURE__*/ _jsx(Typography, {
1561
+ variant: "body2",
1562
+ color: "text.secondary",
1563
+ children: profile.bio
1564
+ }) : null,
1565
+ (()=>{
1566
+ const links = [
1567
+ {
1568
+ key: 'website',
1569
+ label: 'Website',
1570
+ icon: mdiWeb.path,
1571
+ href: safePublisherHref(profile == null ? void 0 : profile.website)
1572
+ },
1573
+ {
1574
+ key: 'support',
1575
+ label: 'Support',
1576
+ icon: mdiLifebuoy.path,
1577
+ href: safePublisherHref(profile == null ? void 0 : profile.supportUrl)
1578
+ },
1579
+ {
1580
+ key: 'github',
1581
+ label: 'GitHub',
1582
+ icon: mdiGithub.path,
1583
+ href: safePublisherHref(profile == null ? void 0 : profile.githubUrl)
1584
+ },
1585
+ {
1586
+ key: 'x',
1587
+ label: 'X',
1588
+ icon: mdiTwitter.path,
1589
+ href: safePublisherHref(profile == null ? void 0 : profile.xUrl)
1590
+ },
1591
+ {
1592
+ key: 'linkedin',
1593
+ label: 'LinkedIn',
1594
+ icon: mdiLinkedin.path,
1595
+ href: safePublisherHref(profile == null ? void 0 : profile.linkedinUrl)
1596
+ }
1597
+ ].filter((entry)=>entry.href);
1598
+ const email = typeof (profile == null ? void 0 : profile.supportEmail) === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profile.supportEmail) ? profile.supportEmail : undefined;
1599
+ if (!links.length && !email) return null;
1600
+ return /*#__PURE__*/ _jsxs(Stack, {
1601
+ direction: "row",
1602
+ spacing: 0.5,
1603
+ children: [
1604
+ email ? /*#__PURE__*/ _jsx(Tooltip, {
1605
+ title: `Support: ${email}`,
1606
+ children: /*#__PURE__*/ _jsx(IconButton, {
1607
+ size: "small",
1608
+ component: "a",
1609
+ href: `mailto:${email}`,
1610
+ "aria-label": `Email support at ${email}`,
1611
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
1612
+ path: mdiEmailOutline.path,
1613
+ fontSize: "small"
1614
+ })
1615
+ })
1616
+ }) : null,
1617
+ links.map((entry)=>/*#__PURE__*/ _jsx(Tooltip, {
1618
+ title: entry.label,
1619
+ children: /*#__PURE__*/ _jsx(IconButton, {
1620
+ size: "small",
1621
+ component: "a",
1622
+ href: entry.href,
1623
+ target: "_blank",
1624
+ rel: "noopener noreferrer",
1625
+ "aria-label": entry.label,
1626
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
1627
+ path: entry.icon,
1628
+ fontSize: "small"
1629
+ })
1630
+ })
1631
+ }, entry.key))
1632
+ ]
1633
+ });
1634
+ })()
1635
+ ]
1636
+ })
1637
+ }),
1638
+ (listing == null ? void 0 : listing.homepageUrl) || (listing == null ? void 0 : listing.repositoryUrl) ? /*#__PURE__*/ _jsx(CardDisplay, {
1639
+ header: 'Links',
1640
+ help: pluginDocsHelp('publisherHandbook', {
1641
+ anchor: '#authoring-your-listing'
1642
+ }),
1643
+ contentGutterX: true,
1644
+ contentGutterY: true,
1645
+ children: /*#__PURE__*/ _jsxs(Stack, {
1646
+ spacing: 0.5,
1647
+ children: [
1648
+ (listing == null ? void 0 : listing.homepageUrl) ? /*#__PURE__*/ _jsx(MuiLink, {
1649
+ href: listing.homepageUrl,
1650
+ target: "_blank",
1651
+ rel: "noopener noreferrer",
1652
+ color: "primary",
1653
+ underline: "hover",
1654
+ variant: "body2",
1655
+ children: 'Homepage'
1656
+ }) : null,
1657
+ (listing == null ? void 0 : listing.repositoryUrl) ? /*#__PURE__*/ _jsx(MuiLink, {
1658
+ href: listing.repositoryUrl,
1659
+ target: "_blank",
1660
+ rel: "noopener noreferrer",
1661
+ color: "primary",
1662
+ underline: "hover",
1663
+ variant: "body2",
1664
+ children: 'Source repository'
1665
+ }) : null
1666
+ ]
1667
+ })
1668
+ }) : null,
1669
+ /*#__PURE__*/ _jsx(CardDisplay, {
1670
+ header: 'Version history',
1671
+ help: pluginDocsHelp('publisherHandbook', {
1672
+ anchor: '#shipping-a-new-version'
1673
+ }),
1674
+ contentGutterX: true,
1675
+ contentGutterY: true,
1676
+ children: versions.length ? /*#__PURE__*/ _jsxs(Stack, {
1677
+ spacing: 1,
1678
+ children: [
1679
+ versions.map((entry, index)=>{
1680
+ var _entry_activeInstalls;
1681
+ return /*#__PURE__*/ _jsxs(Stack, {
1682
+ spacing: 0.25,
1683
+ children: [
1684
+ /*#__PURE__*/ _jsxs(Stack, {
1685
+ direction: "row",
1686
+ spacing: 1,
1687
+ sx: {
1688
+ alignItems: 'center'
1689
+ },
1690
+ children: [
1691
+ /*#__PURE__*/ _jsx(Typography, {
1692
+ variant: "body2",
1693
+ children: `v${entry.version}`
1694
+ }),
1695
+ index === 0 ? /*#__PURE__*/ _jsx(Chip, {
1696
+ size: "small",
1697
+ label: "Latest"
1698
+ }) : null,
1699
+ entry.trust === 'realm' ? /*#__PURE__*/ _jsx(Chip, {
1700
+ size: "small",
1701
+ color: "success",
1702
+ label: "Realm-trusted"
1703
+ }) : null,
1704
+ /*#__PURE__*/ _jsx(Typography, {
1705
+ variant: "caption",
1706
+ color: "text.secondary",
1707
+ children: entry.publishedAtMs ? new Date(entry.publishedAtMs).toLocaleDateString() : ''
1708
+ })
1709
+ ]
1710
+ }),
1711
+ entry.installCount || entry.activeInstalls ? /*#__PURE__*/ _jsxs(Typography, {
1712
+ variant: "caption",
1713
+ color: "text.secondary",
1714
+ children: [
1715
+ `${(_entry_activeInstalls = entry.activeInstalls) != null ? _entry_activeInstalls : 0} on this version`,
1716
+ entry.installCount ? ` · ${entry.installCount} all time` : ''
1717
+ ]
1718
+ }) : null
1719
+ ]
1720
+ }, entry.version);
1721
+ }),
1722
+ (installTotals == null ? void 0 : installTotals.untrackedActiveInstalls) || (installTotals == null ? void 0 : installTotals.untrackedInstallCount) ? /*#__PURE__*/ _jsx(Typography, {
1723
+ variant: "caption",
1724
+ color: "text.secondary",
1725
+ children: `${installTotals.untrackedInstallCount} ` + `install${installTotals.untrackedInstallCount === 1 ? '' : 's'} all-time and ${installTotals.untrackedActiveInstalls} still active predate per-version ` + 'tracking, and are not counted above.'
1726
+ }) : null
1727
+ ]
1728
+ }) : versionHistory.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
1729
+ variant: "body2",
1730
+ color: "text.secondary",
1731
+ children: `Latest version: v${(_ref15 = listing == null ? void 0 : listing.latestVersion) != null ? _ref15 : '…'}`
1732
+ }) : /*#__PURE__*/ _jsx(Stack, {
1733
+ spacing: 0.5,
1734
+ children: versionHistory.map((entry, index)=>{
1735
+ var _entry_publishedAt;
1736
+ return /*#__PURE__*/ _jsxs(Stack, {
1737
+ direction: "row",
1738
+ spacing: 1,
1739
+ sx: {
1740
+ alignItems: 'center'
1741
+ },
1742
+ children: [
1743
+ /*#__PURE__*/ _jsx(Typography, {
1744
+ variant: "body2",
1745
+ children: `v${entry.version}`
1746
+ }),
1747
+ index === 0 ? /*#__PURE__*/ _jsx(Chip, {
1748
+ size: "small",
1749
+ label: "Latest"
1750
+ }) : null,
1751
+ /*#__PURE__*/ _jsx(Typography, {
1752
+ variant: "caption",
1753
+ color: "text.secondary",
1754
+ children: ((_entry_publishedAt = entry.publishedAt) == null ? void 0 : _entry_publishedAt.toDate) ? entry.publishedAt.toDate().toLocaleDateString() : ''
1755
+ })
1756
+ ]
1757
+ }, entry.version);
1758
+ })
1759
+ })
1760
+ }),
1761
+ /*#__PURE__*/ _jsx(ListingReviews, {
1762
+ listingId: listingId,
1763
+ listing: listing
1764
+ }),
1765
+ /*#__PURE__*/ _jsx(ReportTarget, {
1766
+ listingId: listingId,
1767
+ label: String((_ref16 = listing == null ? void 0 : listing.displayName) != null ? _ref16 : 'this listing')
1768
+ })
1769
+ ]
1770
+ })
1771
+ }
1772
+ ]
1773
+ }),
1774
+ /*#__PURE__*/ _jsxs(Dialog, {
1775
+ open: confirmOpen,
1776
+ onClose: ()=>setConfirmOpen(false),
1777
+ maxWidth: "xs",
1778
+ fullWidth: true,
1779
+ children: [
1780
+ /*#__PURE__*/ _jsx(DialogTitle, {
1781
+ children: unreviewedInstall ? 'Install an unreviewed version?' : installed ? 'Update this listing?' : 'Install this listing?'
1782
+ }),
1783
+ /*#__PURE__*/ _jsx(DialogContent, {
1784
+ dividers: true,
1785
+ children: /*#__PURE__*/ _jsxs(Stack, {
1786
+ spacing: 1,
1787
+ children: [
1788
+ /*#__PURE__*/ _jsxs(Typography, {
1789
+ variant: "body2",
1790
+ children: [
1791
+ installed ? 'Update ' : 'Install ',
1792
+ /*#__PURE__*/ _jsx("strong", {
1793
+ children: listing == null ? void 0 : listing.displayName
1794
+ }),
1795
+ listing ? ` (${listingArtifactLabel(listing)}, v${listing == null ? void 0 : listing.latestVersion})` : '',
1796
+ '.'
1797
+ ]
1798
+ }),
1799
+ /*#__PURE__*/ _jsxs(Typography, {
1800
+ variant: "body2",
1801
+ color: "text.secondary",
1802
+ children: [
1803
+ 'This will be installed to ',
1804
+ /*#__PURE__*/ _jsx("strong", {
1805
+ children: installTargetSummary()
1806
+ }),
1807
+ '.'
1808
+ ]
1809
+ }),
1810
+ unreviewedInstall ? /*#__PURE__*/ _jsx(Alert, {
1811
+ severity: "warning",
1812
+ children: `v${listing == null ? void 0 : listing.latestVersion} has not passed review. ` + 'Every site you install it to is publicly reachable — ' + 'there is no staging site here — so unreviewed code ' + 'will serve real visitors until you uninstall it.'
1813
+ }) : null
1814
+ ]
1815
+ })
1816
+ }),
1817
+ /*#__PURE__*/ _jsxs(DialogActions, {
1818
+ children: [
1819
+ /*#__PURE__*/ _jsx(Button, {
1820
+ onClick: ()=>setConfirmOpen(false),
1821
+ children: 'Cancel'
1822
+ }),
1823
+ /*#__PURE__*/ _jsx(Button, {
1824
+ variant: "contained",
1825
+ color: "primary",
1826
+ onClick: runInstall,
1827
+ children: unreviewedInstall ? 'Install for testing' : installed ? 'Update' : 'Install'
1828
+ })
1829
+ ]
1830
+ })
1831
+ ]
1832
+ }),
1833
+ /*#__PURE__*/ _jsx(ArtifactUpdateDialog, {
1834
+ open: updateOpen,
1835
+ onClose: ()=>setUpdateOpen(false),
1836
+ artifactName: (_ref17 = listing == null ? void 0 : listing.displayName) != null ? _ref17 : 'this listing',
1837
+ preview: updatePreview,
1838
+ loading: updateLoading,
1839
+ onApplyMerge: (takePaths, confirmDestructive)=>void runUpdate('merge', takePaths, confirmDestructive),
1840
+ onApplyCopy: ()=>void runCopy()
1841
+ }),
1842
+ /*#__PURE__*/ _jsx(UninstallImpactDialog, {
1843
+ open: pendingUninstall !== null,
1844
+ listing: listing,
1845
+ scope: (_ref18 = pendingUninstall == null ? void 0 : pendingUninstall.scope) != null ? _ref18 : 'host',
1846
+ targets: (_ref19 = pendingUninstall == null ? void 0 : pendingUninstall.targets) != null ? _ref19 : [],
1847
+ onCancel: ()=>setPendingUninstall(null),
1848
+ onConfirm: async ()=>{
1849
+ const target = pendingUninstall;
1850
+ setPendingUninstall(null);
1851
+ if (!target) return;
1852
+ await uninstall(listing, target.scope);
1853
+ setPinsNonce((nonce)=>nonce + 1);
1854
+ }
1855
+ }),
1856
+ /*#__PURE__*/ _jsx(Dialog, {
1857
+ open: Boolean(lightboxUrl),
1858
+ onClose: ()=>setLightboxUrl(null),
1859
+ maxWidth: "lg",
1860
+ children: lightboxUrl ? /*#__PURE__*/ _jsx(Box, {
1861
+ component: "img",
1862
+ src: lightboxUrl,
1863
+ alt: `${listing == null ? void 0 : listing.displayName} screenshot`,
1864
+ onClick: ()=>setLightboxUrl(null),
1865
+ sx: {
1866
+ display: 'block',
1867
+ maxWidth: '100%',
1868
+ maxHeight: '85vh',
1869
+ cursor: 'zoom-out'
1870
+ }
1871
+ }) : null
1872
+ })
1873
+ ]
1874
+ })
1875
+ ]
1876
+ });
1877
+ }
1878
+ export default MarketplaceListingContent;
1879
+
1880
+ //# sourceMappingURL=listing-content.component.js.map