@aglyn/plugins-marketplace 1.0.0-beta.199 → 1.0.0-beta.200

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 (57) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/marketplace-browse.component.js +104 -169
  3. package/src/lib/components/marketplace-browse.component.js.map +1 -1
  4. package/src/lib/components/marketplace-reports-page.component.js +9 -0
  5. package/src/lib/components/marketplace-reports-page.component.js.map +1 -1
  6. package/src/lib/components/org-licences-panel.component.js +212 -170
  7. package/src/lib/components/org-licences-panel.component.js.map +1 -1
  8. package/src/lib/components/org-seller-panel.component.js +23 -5
  9. package/src/lib/components/org-seller-panel.component.js.map +1 -1
  10. package/src/lib/components/plugin-reviews-queue.component.d.ts +9 -0
  11. package/src/lib/components/plugin-reviews-queue.component.js +190 -114
  12. package/src/lib/components/plugin-reviews-queue.component.js.map +1 -1
  13. package/src/lib/hooks/use-listing-name-lookup.d.ts +34 -0
  14. package/src/lib/hooks/use-listing-name-lookup.js +131 -0
  15. package/src/lib/hooks/use-listing-name-lookup.js.map +1 -0
  16. package/src/lib/model/listing-query.d.ts +140 -0
  17. package/src/lib/model/listing-query.js +393 -0
  18. package/src/lib/model/listing-query.js.map +1 -0
  19. package/src/lib/model/marketplace.d.ts +11 -0
  20. package/src/lib/model/marketplace.js.map +1 -1
  21. package/src/lib/server/admin-reports.js +30 -12
  22. package/src/lib/server/admin-reports.js.map +1 -1
  23. package/src/lib/server/admin-reviews.js +180 -128
  24. package/src/lib/server/admin-reviews.js.map +1 -1
  25. package/src/lib/server/admin-trust.js +2 -1
  26. package/src/lib/server/admin-trust.js.map +1 -1
  27. package/src/lib/server/billing-webhook.js +13 -1
  28. package/src/lib/server/billing-webhook.js.map +1 -1
  29. package/src/lib/server/install-email-starter.js +9 -2
  30. package/src/lib/server/install-email-starter.js.map +1 -1
  31. package/src/lib/server/install-layout.js +17 -5
  32. package/src/lib/server/install-layout.js.map +1 -1
  33. package/src/lib/server/install-template.js +15 -5
  34. package/src/lib/server/install-template.js.map +1 -1
  35. package/src/lib/server/install.js +10 -2
  36. package/src/lib/server/install.js.map +1 -1
  37. package/src/lib/server/listing-query-fields.d.ts +38 -0
  38. package/src/lib/server/listing-query-fields.js +50 -0
  39. package/src/lib/server/listing-query-fields.js.map +1 -0
  40. package/src/lib/server/publish-dataset-schema.js +6 -2
  41. package/src/lib/server/publish-dataset-schema.js.map +1 -1
  42. package/src/lib/server/publish-email-starter.js +6 -2
  43. package/src/lib/server/publish-email-starter.js.map +1 -1
  44. package/src/lib/server/publish-email-template.js +6 -2
  45. package/src/lib/server/publish-email-template.js.map +1 -1
  46. package/src/lib/server/publish-layout.js +6 -2
  47. package/src/lib/server/publish-layout.js.map +1 -1
  48. package/src/lib/server/publish-plugin.js +75 -7
  49. package/src/lib/server/publish-plugin.js.map +1 -1
  50. package/src/lib/server/publish-template.js +6 -2
  51. package/src/lib/server/publish-template.js.map +1 -1
  52. package/src/lib/server/publish-theme.js +6 -2
  53. package/src/lib/server/publish-theme.js.map +1 -1
  54. package/src/lib/server/publish.js +6 -2
  55. package/src/lib/server/publish.js.map +1 -1
  56. package/src/lib/server/update-artifact.js +18 -4
  57. package/src/lib/server/update-artifact.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/marketplace-browse.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { mdiCheckDecagram } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n Alert,\n Chip,\n Tooltip,\n Grid,\n MenuItem,\n Rating,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n documentId,\n getDoc,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport {\n buildRoute,\n type OrgPermissions,\n PLATFORM_BRAND_NAME,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useHostOrgId,\n useUser,\n useScopeTokens,\n} from '@aglyn/tenant-feature-instance'\n/*\n * The MODULE, not the barrel, for the two PURE helpers — the specs that render\n * this grid mock `@aglyn/tenant-feature-instance` wholesale to stage their\n * Firestore hooks, and a query builder imported through that barrel disappears\n * under the mock. Neither of these is a hook.\n */\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n listingArtifactType,\n listingArtifactLabel,\n resolvePluginInstallState,\n} from '../model/marketplace'\nimport { ListingImage } from './listing-image.component'\n\n// The console route table is shared (AGL-685), so these go through\n// buildRoute rather than being reassembled from a base string — the shape\n// of `/[orgSlug]/hosts/[host]/marketplace/…` is not this plugin's to know.\n\n/**\n * How many install records the shelf reads to decide what it already runs.\n *\n * One number for the five collections that answer that question, because the\n * card reads them together and a reader comparing a plugin's chip to a\n * dataset's should not have to know which window was twice the other. It\n * bounds an install-state LOOKUP, not the shelf: the listings query has its\n * own cap, and the grid pages what survives the filters.\n */\nconst INSTALL_STATE_CEILING = 100\n\nexport interface MarketplaceBrowseProps {\n hostId: string\n /** Signed-in user's org permissions, supplied by the shell (AGL-395). */\n permissions?: Partial<OrgPermissions>\n /**\n * Rendered inside the org-scope `/marketplace` route (AGL-772) rather\n * than a site's marketplace tab. Only affects link targets — the grid\n * still installs through the acting `hostId` until targeting lands\n * (AGL-773) — so detail links resolve to the org route, not a per-site\n * one that is being retired.\n */\n orgScoped?: boolean\n /**\n * The acting org's slug from the URL (AGL-867). When given at org scope,\n * detail links build from it directly instead of the async\n * `hostIndex`→`orgs` resolution, which can return empty and leave the detail\n * page — the only place installs happen now — unreachable from browse.\n */\n orgSlug?: string\n /**\n * Restrict the grid to one publisher's listings (AGL-869), for the org-scope\n * publisher page. Omitted on the main browse, which shows everyone.\n */\n publisherId?: string\n}\n\n/**\n * Marketplace components browse (AGL-44). A read-only catalogue: each card links\n * to the listing's detail page, which is the only place an install happens\n * (AGL-867) — installing from a grid card was too easy and skipped the\n * site-targeting choice. The card still shows install STATE (installed,\n * org-wide) so the shelf reads honestly, but carries no install action.\n */\nexport function MarketplaceBrowse(props: MarketplaceBrowseProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { orgScoped, orgSlug: orgSlugProp, publisherId } = props\n const [handles, setHandles] = useState<Record<string, string>>({})\n // Listings are org-owned (AGL-652), so \"is this mine\" is an org comparison.\n // Resolved from the routing mirror rather than a new prop so the component\n // stays self-contained; hostIndex is signed-in readable.\n const [viewerOrgId, setViewerOrgId] = useState<string | null>(null)\n useEffect(() => {\n let active = true\n void getDoc(doc(firestore, 'hostIndex', hostId))\n .then((snapshot) => {\n if (active) setViewerOrgId((snapshot.get('orgId') as string) ?? null)\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId])\n // Card links were built as `/{hostDocId}/marketplace/…`, a shape that has\n // not resolved since AGL-621/622 — every listing and publisher link on\n // this grid 404'd. One shared resolution (AGL-673); null renders plain\n // text rather than a link to nowhere.\n const { orgSlug: resolvedOrgSlug, subdomain } = useConsoleHostRoute(hostId)\n // Prefer the URL-supplied slug (AGL-867) — synchronous and always present at\n // org scope — over the async host resolution, which the per-site route still\n // relies on.\n const orgSlug = orgSlugProp ?? resolvedOrgSlug\n\n // Link targets differ by surface (AGL-772): the org marketplace resolves\n // to the org route (`/[orgSlug]/marketplace/[listingId]`), the per-site tab\n // to the host route. Null → plain text, never a link to nowhere. Publisher\n // pages have no org route yet, so they stay text at org scope for now.\n const listingHref = (listingId: string) =>\n orgScoped\n ? orgSlug\n ? buildRoute(Route.ORG_MARKETPLACE_LISTING, { orgSlug, listingId })\n : undefined\n : orgSlug && subdomain\n ? buildRoute(Route.ORG_MARKETPLACE_LISTING, {\n orgSlug,\n listingId,\n })\n : undefined\n // Storefront links carry the publisher's HANDLE (AGL-1001), the identity\n // shown right beside them on the card. The id remains a valid segment on\n // the page itself, so a publisher whose handle hasn't loaded yet still\n // links somewhere real rather than nowhere.\n const publisherHref = (profileId: string) =>\n orgScoped\n ? // Org-scope publisher storefront (AGL-869): all of one publisher's\n // listings. Needs the URL slug, which is passed in at org scope.\n orgSlug\n ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {\n orgSlug,\n handle: handles[profileId] ?? profileId,\n })\n : undefined\n : orgSlug && subdomain\n ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {\n orgSlug,\n handle: handles[profileId] ?? profileId,\n })\n : undefined\n\n /**\n * NO `where('deletedAt','==',null)` HERE — soft-deleted listings are dropped\n * in `items` below instead (AGL-1196).\n *\n * `deletedAt` flips every time a publisher unpublishes or republishes. A\n * document that stops matching a live query leaves the query target, and the\n * client can cache a `noDocument` tombstone at its own path — which is then\n * served to every reader of that path, including the detail page, which\n * reads `marketplaceListings/{id}` BY ID. So a browse session open across an\n * unpublish could 404 a listing that exists, and republishing would not\n * clear it: a resumed listen only pulls deltas, so an otherwise-unchanged\n * document is never re-sent (AGL-827, AGL-929).\n *\n * Dropping the predicate removes the mechanism rather than healing it: with\n * no `where`, no document can stop matching. The rule permits it —\n * `marketplaceListings` is `allow read: if true`, with no `resource.data`\n * term, so an unconstrained list is not denied. (That is NOT true of the\n * scoped `datasets` query below, which is why this fix does not generalise.)\n *\n * It also fixes a quieter bug: `== null` matches only an EXPLICIT null, and\n * Firestore cannot express \"field is absent\". Every publish path stamps\n * `deletedAt: null` today, so this happens to be exact — but any future\n * write path that omits the field would make a live listing silently\n * invisible. An in-memory falsy check treats absent as live, which is what\n * the dataset and email-template filters in this file already do.\n *\n * The cap counts CANDIDATES, not results — private and unreviewed listings\n * already consume slots and get filtered below, so this is the same class of\n * consumption, nudged up to leave room for soft-deleted rows. Measured on\n * production 2026-08-03: 7 listings, 0 soft-deleted, 0 missing the field.\n *\n * ## The cap DOES take an `orderBy`, and it is not the same decision\n *\n * A `limit` with no ordering is answered in document-id order, and listing\n * ids are random — so the ninety this loaded were an arbitrary ninety, and\n * the client sort below then arranged that sample by date and called it\n * \"Newest\". The window is now the newest ninety, which is what the default\n * sort already claimed it was.\n *\n * `orderBy` carries the drop the `where` above was removed to avoid — it\n * matches only documents that HAVE the field, so a listing without\n * `createdAt` would not be mis-ordered, it would be invisible. That is safe\n * here for a reason the `deletedAt` predicate could not offer: a listing is\n * created by exactly two paths, `publish.ts` and `publish-plugin.ts`, and\n * both stamp `createdAt` in the same `set` that brings the document into\n * existence. Every other writer resolves an EXISTING listing by id and\n * refuses when it is absent, so none of them can produce one without the\n * field.\n *\n * It also cannot reproduce the tombstone (AGL-827/929) that made a mutable\n * `where` unsafe: `createdAt` is written under `existing.empty` and never\n * rewritten, so no document can stop matching mid-session and leave a\n * `noDocument` cached at a path the detail page reads by id.\n */\n const { data: listings } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'marketplaceListings'),\n orderBy('createdAt', 'desc'),\n limit(90),\n ),\n [firestore],\n { idField: '$id' },\n )\n /*\n * ## The install-state lookups are ORDERED too, and they are not lists\n *\n * The five queries below (this one, the two pin collections, the org\n * datasets and the site's email templates) answer one question per card:\n * has this listing already been installed here. None of them is rendered —\n * each is folded into a map keyed by listing id — so the fix that suits the\n * shelf above does not suit them: there is no page to walk and no footer to\n * put a control in.\n *\n * What they share with it is the defect. A bare `limit` is answered in\n * DOCUMENT-ID order, so an unnamed window is an arbitrary slice of its\n * collection, and a lookup MISS is indistinguishable from a genuine\n * not-installed: a workspace past such a window is shown \"Add to this site\"\n * beside something it already runs, and the detail page — which reads its\n * own pins by id — disagrees with the grid it was reached from.\n *\n * `collectionCeiling` names the order and asks for one row past the\n * ceiling, which is what turns \"the window may have bitten\" into a fact the\n * shelf can state rather than a silence it cannot.\n */\n const { data: installedRead } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'components'),\n INSTALL_STATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: installedDocs, truncated: installedTruncated } = useMemo(\n () => ceilingedWindow<any>(installedRead, INSTALL_STATE_CEILING),\n [installedRead],\n )\n\n // listingId → installed component doc (deleted installs don't count).\n const installed = useMemo(() => {\n const map: Record<string, any> = {}\n for (const definition of installedDocs ?? []) {\n const listingId = definition?.marketplace?.listingId\n if (listingId && !definition.deletedAt) map[listingId] = definition\n }\n return map\n }, [installedDocs])\n\n // Plugin installs are version PINS, not component snapshots (AGL-656): the\n // `components` map above never holds one, so a plugin already installed —\n // at host or org scope — showed \"Add to this site\". These two pin\n // collections are what the loader honors (a host pin shadows an org pin).\n const orgId = useHostOrgId(hostId)\n const { data: hostPinRead } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'installs'),\n INSTALL_STATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: hostPinDocs, truncated: hostPinsTruncated } = useMemo(\n () => ceilingedWindow<any>(hostPinRead, INSTALL_STATE_CEILING),\n [hostPinRead],\n )\n // Held at null while `useHostOrgId` is in flight, never `orgs/-pending-`\n // (AGL-1440): the AGL-1047 comment seven lines below records the same\n // denial-every-mount shape for `useScopeTokens` — this listen had it too.\n const { data: orgPinRead } = useFirestoreCollection<any>(\n () =>\n orgId\n ? collectionCeiling(\n collection(firestore, 'orgs', orgId, 'installs'),\n INSTALL_STATE_CEILING,\n )\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n const { rows: orgPinDocs, truncated: orgPinsTruncated } = useMemo(\n () => ceilingedWindow<any>(orgPinRead, INSTALL_STATE_CEILING),\n [orgPinRead],\n )\n // Scoped (AGL-1044): an unfiltered list is REJECTED for a scoped member,\n // not filtered, so this would error without the constraint.\n //\n // Wait for the member doc before listing (AGL-1047). `useScopeTokens`\n // reports `orgWide: true` while loading, so without `scopeLoaded` a scoped\n // collaborator's first render computes `needsScope: false` and sends an\n // UNFILTERED list that the AGL-1041 rules deny per document. It recovers\n // on the next render, which is what makes it easy to miss: the page looks\n // right and logs a denial every mount.\n const {\n tokens: scopeTokens,\n orgWide: viewerOrgWide,\n loaded: scopeLoaded,\n } = useScopeTokens(orgId ?? undefined)\n const needsScope = Boolean(orgId) && !viewerOrgWide\n // The AGL-657 types land in neither the components collection nor a pin\n // (AGL-789): a dataset schema becomes an org dataset, an email template a\n // draft version. Both installers stamp the source listing, so read those.\n /*\n * `documentId()` rather than a field, for the reason the audience sweep in\n * `campaign-send.ts` gives: Firestore's automatic single-field index for an\n * array member is keyed on the value and the document name, so\n * `array-contains-any` plus `orderBy(__name__)` is served by it. Ordering on\n * anything else would need a composite index per scope shape.\n *\n * The ceiling is the shared one. It is not sized for the dataset\n * collection: it is sized for the number of LISTINGS whose install state a\n * shelf of ninety cards can describe, which is the same question its four\n * neighbours answer, so it is the same number.\n */\n const { data: datasetRead } = useFirestoreCollection<any>(\n () =>\n orgId && scopeLoaded\n ? query(\n collection(firestore, 'orgs', orgId, 'datasets'),\n ...(needsScope\n ? [where('visibleTo', 'array-contains-any', scopeTokens)]\n : []),\n orderBy(documentId()),\n limit(INSTALL_STATE_CEILING + 1),\n )\n : null,\n [firestore, orgId, scopeLoaded, needsScope, scopeTokens],\n // `visibleTo` is MUTABLE — every scope edit rewrites it — so this query\n // can tombstone a dataset the way AGL-827 tombstoned a host. The rule\n // requires the constraint for anyone who is not org-wide, so unlike the\n // listings query above the predicate cannot simply be dropped (AGL-1196).\n { idField: '$id', confirmDisappearances: true },\n )\n const { rows: datasetDocs, truncated: datasetsTruncated } = useMemo(\n () => ceilingedWindow<any>(datasetRead, INSTALL_STATE_CEILING),\n [datasetRead],\n )\n const { data: emailRead } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'emailTemplates'),\n INSTALL_STATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: emailDocs, truncated: emailsTruncated } = useMemo(\n () => ceilingedWindow<any>(emailRead, INSTALL_STATE_CEILING),\n [emailRead],\n )\n // A theme is not a document in a collection — it is a field on the site\n // (AGL-1020) — so its install is read from the host doc rather than from a\n // per-artifact query like the two above.\n const { data: hostDoc } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n // listingId → the newest install of it. A schema install deliberately makes\n // a NEW dataset every time, so this is genuinely one-to-many; the highest\n // installed version is what the card should speak for.\n const artifactInstalls = useMemo(() => {\n const map: Record<string, { version: string | null }> = {}\n const note = (listingId: unknown, version: unknown) => {\n const id = listingId ? String(listingId) : ''\n if (!id) return\n const next = version != null ? String(version) : null\n const seen = map[id]?.version\n if (!seen || (next && next > seen)) map[id] = { version: next }\n }\n for (const dataset of datasetDocs ?? []) {\n if (dataset.deletedAt) continue\n note(dataset.source?.listingId, dataset.source?.version)\n }\n for (const template of emailDocs ?? []) {\n if (template.deletedAt) continue\n note(template.installedFrom?.listingId, template.installedFrom?.version)\n }\n note(hostDoc?.themeInstalledFrom?.listingId, hostDoc?.themeInstalledFrom?.version)\n return map\n }, [datasetDocs, emailDocs, hostDoc])\n\n const hostPins = useMemo(() => {\n const map: Record<string, any> = {}\n for (const pin of hostPinDocs ?? []) map[pin.$id] = pin\n return map\n }, [hostPinDocs])\n const orgPins = useMemo(() => {\n const map: Record<string, any> = {}\n for (const pin of orgPinDocs ?? []) map[pin.$id] = pin\n return map\n }, [orgPinDocs])\n\n // Resolve each listing's publisher handle once. `handles` must NOT be a\n // dependency here: the effect writes `handles`, so listing it would make the\n // effect re-run on its own output. During the post-load window, when\n // `listings` arrives while every other subscription on this always-mounted\n // grid is also settling, that self-retrigger adds render+effect cycles to an\n // already dense flurry — enough that a concurrent update elsewhere can trip\n // React's nested-update limit (AGL-785). A ref of already-requested ids\n // dedupes instead, so the effect depends only on `listings`.\n const requestedHandles = useRef<Set<string>>(new Set())\n useEffect(() => {\n const profileIds = [\n ...new Set((listings ?? []).map((listing: any) => listing.profileId)),\n ].filter(\n (profileId) =>\n profileId && !requestedHandles.current.has(String(profileId)),\n )\n if (!profileIds.length) return\n for (const profileId of profileIds)\n requestedHandles.current.add(String(profileId))\n let cancelled = false\n Promise.all(\n profileIds.map(async (profileId) => {\n const snapshot = await getDoc(\n doc(firestore, 'publisherProfiles', String(profileId)),\n ).catch(() => null)\n return [profileId, snapshot?.get('handle') ?? ''] as const\n }),\n ).then((entries) => {\n if (!cancelled) {\n setHandles((prev) => ({\n ...prev,\n ...Object.fromEntries(entries),\n }))\n }\n })\n return () => {\n cancelled = true\n }\n }, [listings, firestore])\n\n // Browse controls (AGL-95): client-side search/filter/sort over the\n // fetched page of listings.\n const [search, setSearch] = useState('')\n const [category, setCategory] = useState<string | null>(null)\n const [sort, setSort] = useState<'newest' | 'installed' | 'rated'>(\n 'newest',\n )\n const categories = useMemo(\n () =>\n [\n ...new Set(\n (listings ?? [])\n .map((listing: any) => listing.category)\n .filter(Boolean),\n ),\n ].sort() as string[],\n [listings],\n )\n const items = useMemo(() => {\n const needle = search.trim().toLowerCase()\n const filtered = (listings ?? []).filter((listing: any) => {\n // Private plugins never appear here, not even for the org that owns\n // them (AGL-968/993). The owner exemption below exists so a publisher\n // can watch their own SUBMISSION move through review — but a private\n // listing is not waiting to be listed, it is deliberately not for the\n // marketplace, and showing it in this grid is the one thing \"private\"\n // promises will not happen. Owners reach theirs from\n // Marketplace › Listings, whose View opens the same detail page\n // installs happen on.\n // Soft-deleted listings used to be excluded by the query itself; they\n // are dropped here now so no mutable field sits in a `where`\n // (AGL-1196). Unconditional — unlike the review gate below, deletion\n // has no owner exemption.\n if (isListingDeleted(listing)) return false\n if (isPrivateListing(listing)) return false\n // Review queue gate (AGL-432): unreviewed/rejected plugin listings\n // stay off the public browse; the owner still sees their own (the\n // detail page shows them the status). UX-level only — the docs are\n // public-readable by design.\n if (!isListingBrowsable(listing) && listing.profileId !== viewerOrgId) {\n return false\n }\n // Publisher page (AGL-869): only this publisher's listings.\n if (publisherId && listing.profileId !== publisherId) return false\n if (category && listing.category !== category) return false\n if (!needle) return true\n return [listing.displayName, listing.description, listing.category]\n .filter(Boolean)\n .some((value: string) => value.toLowerCase().includes(needle))\n })\n return [...filtered].sort((a: any, b: any) => {\n if (sort === 'installed') {\n return (b.installCount ?? 0) - (a.installCount ?? 0)\n }\n if (sort === 'rated') {\n // Unrated listings sort last rather than as zero-stars — a new\n // listing has not been judged badly, it has not been judged.\n const byAverage =\n (b.ratingAverage ?? -1) - (a.ratingAverage ?? -1)\n return byAverage || (b.ratingCount ?? 0) - (a.ratingCount ?? 0)\n }\n return (b.createdAt?.seconds ?? 0) - (a.createdAt?.seconds ?? 0)\n })\n }, [listings, search, category, sort, publisherId, user?.uid])\n\n /*\n * The shelf PAGES (AGL-2501). It used to render every card the window held\n * in one wall, so the only control over how much arrived at once was the\n * cap in the query — a number the reader cannot see and cannot change.\n *\n * Client-side, deliberately, and this is the case `ListPagination`'s\n * `count` prop exists for. The search box, the category chips and the\n * owner/review gates are all resolved in memory, so a server page would be\n * a page of CANDIDATES: a reader filtering to one category would get pages\n * that were mostly empty and a count that meant nothing. Paging what is\n * left after the filter is the only version whose numbers are about what is\n * on screen.\n */\n /**\n * The install-state lookups did not read everything they were asked about.\n *\n * Said on the SHELF rather than on the card that is wrong, because there is\n * no way to know which card that is: a miss and a genuine not-installed are\n * the same absence from the same map. What the reader can act on is that the\n * chips are a floor — anything marked installed is installed, and something\n * unmarked may still be.\n */\n const installStateTruncated =\n installedTruncated ||\n hostPinsTruncated ||\n orgPinsTruncated ||\n datasetsTruncated ||\n emailsTruncated\n\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n /*\n * A new query starts at page one. Page four of a category with two pages\n * does not exist, and an out-of-range page renders as an empty grid with\n * nothing saying why — which reads as the filter having broken.\n */\n useEffect(() => {\n setPage(0)\n }, [search, category, sort, pageSize])\n const shown = useMemo(\n () => items.slice(page * pageSize, (page + 1) * pageSize),\n [items, page, pageSize],\n )\n\n return (\n <CardDisplay\n header={'Marketplace components'}\n help={pluginDocsHelp('plugins', { anchor: '#install--upgrade' })}\n contentGutterX\n contentGutterY\n >\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ mb: 2, alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <TextField\n placeholder=\"Search components…\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n size=\"small\"\n sx={{ minWidth: 200 }}\n />\n {categories.map((value) => (\n <Chip\n key={value}\n label={value}\n variant={category === value ? 'filled' : 'outlined'}\n color={category === value ? 'primary' : 'default'}\n onClick={() =>\n setCategory((previous) => (previous === value ? null : value))\n }\n />\n ))}\n <TextField\n value={sort}\n onChange={(event) => setSort(event.target.value as any)}\n size=\"small\"\n select\n sx={{ ml: 'auto', minWidth: 150 }}\n >\n <MenuItem value=\"newest\">{'Newest'}</MenuItem>\n <MenuItem value=\"installed\">{'Most installed'}</MenuItem>\n <MenuItem value=\"rated\">{'Highest rated'}</MenuItem>\n </TextField>\n </Stack>\n {installStateTruncated ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {`Installed state is resolved from the first ${INSTALL_STATE_CEILING} ` +\n 'install records in this workspace, ordered by id. There are ' +\n 'more, so a card may not be marked installed even though it is. ' +\n 'Open a listing to see what this workspace actually runs.'}\n </Alert>\n ) : null}\n {items.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No marketplace components published yet — publish one of your ' +\n 'reusable components from the Setup page to be the first.'}\n </Typography>\n ) : (\n <>\n <Grid container spacing={2}>\n {shown.map((listing: any) => {\n const artifactType = listingArtifactType(listing)\n const isPlugin = artifactType === 'plugin'\n const pluginState = resolvePluginInstallState(\n listing.latestVersion,\n isPlugin ? hostPins[listing.$id] : null,\n isPlugin ? orgPins[listing.$id] : null,\n )\n const componentInstall = installed[listing.$id]\n // datasetSchema/emailTemplate installs are tracked by the source\n // listing stamped on what they created (AGL-789).\n //\n // `emailStarter` is deliberately absent. Every other type has ONE\n // install per site to point at — a pin, a dataset, a draft on a\n // fixed catalog key, the site's theme — so \"Installed\" names\n // something. A starter installs as a NEW email each time, and a\n // site may keep five of them; there is no single copy for a chip\n // to describe, and marking the listing installed would grey out\n // the button that makes the sixth.\n const artifactInstall =\n artifactType === 'datasetSchema' ||\n artifactType === 'emailTemplate' ||\n artifactType === 'theme'\n ? artifactInstalls[listing.$id]\n : undefined\n const isInstalled = isPlugin\n ? pluginState.scope != null\n : Boolean(componentInstall ?? artifactInstall)\n const installedVersion = isPlugin\n ? pluginState.installedVersion\n : (componentInstall?.marketplace?.version ??\n artifactInstall?.version)\n const priceUsd = Number(listing.priceUsd ?? 0)\n const detailHref = listingHref(listing.$id)\n return (\n <Grid key={listing.$id} size={{ xs: 12, sm: 6, md: 4 }}>\n <Stack\n spacing={1}\n sx={{\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n p: 2,\n height: '100%',\n }}\n >\n {/* Resolved, never raw (AGL-1424) — the stored value may be\n a `media:` reference. */}\n <ListingImage\n src={listing.previewImageUrl}\n alt={`${listing.displayName} preview`}\n sx={{\n width: '100%',\n height: 120,\n objectFit: 'cover',\n borderRadius: 1,\n }}\n />\n {/* The name gets the whole line (AGL-1002). Sharing a row\n with the chips meant the title was the only thing that\n gave way when they did not fit — \"Promo Countdown\"\n truncated to \"Promo Coun…\" beside chips with room to\n spare, which inverts what matters on a browse card. */}\n <AppLink\n href={listingHref(listing.$id)}\n color=\"inherit\"\n underline=\"hover\"\n variant=\"subtitle2\"\n >\n {listing.displayName}\n </AppLink>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n {/* Primary classification, said first (AGL-864). */}\n <Chip\n size=\"small\"\n color=\"primary\"\n label={listingArtifactLabel(listing)}\n />\n {listing.category ? (\n <Chip size=\"small\" label={listing.category} />\n ) : null}\n {/* The reviewed badge belongs HERE most of all\n (AGL-1002): the detail page showed it, but browse is\n where someone is comparing options and deciding whose\n code to run.\n\n TWO CLAIMS, SHOWN SEPARATELY (AGL-1121). This chip\n vouches for the PUBLISHER; the one beside it says\n whether THESE bytes were reviewed. They were a single\n \"Verified\" chip driven by `reviewStatus`, which lives\n on the listing and deliberately survives a version\n bump — so a publisher verified on v1.0.0 could ship\n v1.9.0 containing anything and still be badged as\n though the code had been read.\n\n Both tooltips name `PLATFORM_BRAND_NAME` rather than\n the viewing org's `productName` (AGL-2351). They say\n who did the verifying and the reading — the\n deployment's own review queue, one catalog shared by\n every org — and a white-label agency's brand here would\n claim the agency vetted a publisher it has never heard\n of. */}\n {listing.reviewStatus === 'verified' ? (\n <Tooltip\n title={\n `A human at ${PLATFORM_BRAND_NAME} confirmed who ` +\n 'this publisher is, ' +\n 'and that the listing describes what the code does. ' +\n 'It is a claim about the publisher, not about this ' +\n 'release — it survives a version bump.'\n }\n >\n <Chip\n size=\"small\"\n color=\"info\"\n label=\"Verified publisher\"\n // Carries more weight than the neighbouring\n // classification chips on purpose: it is the only one\n // that says a human vouched for the code, and the\n // rest are just taxonomy. The icon is what makes it\n // findable when scanning a grid rather than reading\n // one card.\n icon={\n <MdiIcon\n path={mdiCheckDecagram.path}\n sx={{ fontSize: 16 }}\n />\n }\n sx={{ fontWeight: 600 }}\n />\n </Tooltip>\n ) : null}\n {/* The bytes on offer, not the person who wrote them.\n Absent on a listing published before this field\n existed, which reads as \"not reviewed\" — the safe\n direction for a claim about code. */}\n {listing.latestVersionReviewState === 'approved' ? (\n <Tooltip\n title={\n `A human at ${PLATFORM_BRAND_NAME} read these exact ` +\n 'bytes — the ' +\n 'version on offer — against a required checklist. ' +\n 'Re-earned per version, so a new release starts ' +\n 'without it. Not a security guarantee: every plugin ' +\n 'runs in the same sandbox either way.'\n }\n >\n <Chip size=\"small\" color=\"success\" label=\"Reviewed\" />\n </Tooltip>\n ) : null}\n {/*\n Price on EVERY card, `Free` included (AGL-2173). The\n chip only rendered above zero, so the four free\n listings the marketplace mockup shows carried no\n price at all — and \"no chip\" is the one reading a\n shopper cannot distinguish from \"we forgot\", on the\n row where the paid listing beside it says $29.\n */}\n <Chip\n size=\"small\"\n color={priceUsd > 0 ? 'primary' : 'success'}\n variant={priceUsd > 0 ? 'filled' : 'outlined'}\n label={priceUsd > 0 ? `$${priceUsd}` : 'Free'}\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`v${listing.latestVersion}`}\n {handles[listing.profileId] ? (\n <>\n {' · by '}\n <AppLink\n href={publisherHref(listing.profileId)}\n color=\"primary\"\n underline=\"hover\"\n >\n {`@${handles[listing.profileId]}`}\n </AppLink>\n </>\n ) : (\n ''\n )}\n {listing.installCount\n ? ` · ${listing.installCount} install${\n listing.installCount === 1 ? '' : 's'\n }`\n : ''}\n </Typography>\n {/*\n Stars, as the mockup draws them (AGL-2173). The count\n stays alongside the average: \"5.0\" from one rating and\n from forty are not the same claim (AGL-655). An unrated\n listing says so rather than rendering nothing — silence\n read as \"no stars\", which is the opposite of \"not yet\n rated\".\n */}\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n {listing.ratingCount ? (\n <>\n <Rating\n size=\"small\"\n readOnly\n precision={0.1}\n value={Number(listing.ratingAverage ?? 0)}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${listing.ratingAverage ?? 0} (${listing.ratingCount})`}\n </Typography>\n </>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not yet rated'}\n </Typography>\n )}\n </Stack>\n {listing.description ? (\n <Typography\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ flex: 1 }}\n // Four lines, then ellipsis (AGL-1002). Grid rows\n // stretch to their tallest card, so one publisher's\n // essay used to leave every card beside it mostly white\n // space; the full text is one click away.\n //\n // Plain `style`, not `sx`: emotion drops the\n // `-webkit-box` display value on the way through, and\n // without it the other three properties clamp nothing\n // (measured — `display` computed to `flow-root` while\n // line-clamp and box-orient came through fine). Nothing\n // here reads the theme, so there is nothing to lose.\n style={{\n display: '-webkit-box',\n WebkitBoxOrient: 'vertical',\n WebkitLineClamp: 4,\n overflow: 'hidden',\n }}\n >\n {listing.description}\n </Typography>\n ) : (\n <span style={{ flex: 1 }} />\n )}\n {/* Read-only state, then a link to the detail page — the\n only place an install happens (AGL-867). No install/buy\n action lives on the grid. */}\n {/* Install state reads as one statement (AGL-1002): the\n scope belongs with \"Installed\", not up in the chip row\n among the type and category pills, which say what the\n listing IS rather than what this workspace has done\n with it. Org-wide installs apply to every site\n (AGL-656). */}\n {isInstalled ? (\n <Stack\n direction=\"row\"\n spacing={0.75}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Typography variant=\"caption\" color=\"success.main\">\n {`Installed${\n installedVersion ? ` (v${installedVersion})` : ''\n }`}\n </Typography>\n {isPlugin && pluginState.scope === 'org' ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n pluginState.shadowed\n ? 'Org-wide (shadowed)'\n : 'Org-wide'\n }\n />\n ) : null}\n </Stack>\n ) : null}\n <AppLink\n componentVariant=\"button\"\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n href={detailHref ?? ''}\n disabled={!detailHref}\n >\n {priceUsd > 0 && !isInstalled\n ? `View details · $${priceUsd}`\n : 'View details'}\n </AppLink>\n </Stack>\n </Grid>\n )\n })}\n </Grid>\n {/* `count` is the FILTERED total, which this list genuinely knows —\n it is the array it just sliced. Handing MUI the window's size\n instead would count listings the reader has filtered out. */}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={items.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </CardDisplay>\n )\n}\nMarketplaceBrowse.displayName = 'MarketplaceBrowse'\n\nexport default MarketplaceBrowse\n"],"names":["mdiCheckDecagram","AppLink","CardDisplay","MdiIcon","ListPagination","TABLE_PAGE_SIZE_DEFAULT","Alert","Chip","Tooltip","Grid","MenuItem","Rating","Stack","TextField","Typography","collection","doc","documentId","getDoc","limit","orderBy","query","where","useEffect","useMemo","useRef","useState","buildRoute","PLATFORM_BRAND_NAME","pluginDocsHelp","Route","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","useHostOrgId","useUser","useScopeTokens","ceilingedWindow","collectionCeiling","isListingBrowsable","isListingDeleted","isPrivateListing","listingArtifactType","listingArtifactLabel","resolvePluginInstallState","ListingImage","INSTALL_STATE_CEILING","MarketplaceBrowse","props","hostId","firestore","data","user","orgScoped","orgSlug","orgSlugProp","publisherId","handles","setHandles","viewerOrgId","setViewerOrgId","active","then","snapshot","get","catch","undefined","resolvedOrgSlug","subdomain","listingHref","listingId","ORG_MARKETPLACE_LISTING","publisherHref","profileId","ORG_MARKETPLACE_PUBLISHER","handle","listings","idField","installedRead","rows","installedDocs","truncated","installedTruncated","installed","map","definition","marketplace","deletedAt","orgId","hostPinRead","hostPinDocs","hostPinsTruncated","orgPinRead","orgPinDocs","orgPinsTruncated","tokens","scopeTokens","orgWide","viewerOrgWide","loaded","scopeLoaded","needsScope","Boolean","datasetRead","confirmDisappearances","datasetDocs","datasetsTruncated","emailRead","emailDocs","emailsTruncated","hostDoc","artifactInstalls","note","version","id","String","next","seen","dataset","source","template","installedFrom","themeInstalledFrom","hostPins","pin","$id","orgPins","requestedHandles","Set","profileIds","listing","filter","current","has","length","add","cancelled","Promise","all","entries","prev","Object","fromEntries","search","setSearch","category","setCategory","sort","setSort","categories","items","needle","trim","toLowerCase","filtered","displayName","description","some","value","includes","a","b","installCount","byAverage","ratingAverage","ratingCount","createdAt","seconds","uid","installStateTruncated","page","setPage","pageSize","setPageSize","shown","slice","header","help","anchor","contentGutterX","contentGutterY","direction","spacing","sx","mb","alignItems","flexWrap","rowGap","placeholder","onChange","event","target","size","minWidth","label","variant","color","onClick","previous","select","ml","severity","container","componentInstall","artifactType","isPlugin","pluginState","latestVersion","artifactInstall","isInstalled","scope","installedVersion","priceUsd","Number","detailHref","xs","sm","md","border","borderColor","borderRadius","p","height","src","previewImageUrl","alt","width","objectFit","href","underline","reviewStatus","title","icon","path","fontSize","fontWeight","latestVersionReviewState","readOnly","precision","flex","style","display","WebkitBoxOrient","WebkitLineClamp","overflow","span","shadowed","componentVariant","disabled","rowCount","count","onPageChange","onPageSizeChange"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/marketplace-browse.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { mdiCheckDecagram } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Alert,\n Chip,\n Tooltip,\n Grid,\n MenuItem,\n Rating,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n documentId,\n getDoc,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport {\n buildRoute,\n type OrgPermissions,\n PLATFORM_BRAND_NAME,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useHostOrgId,\n useScopeTokens,\n} from '@aglyn/tenant-feature-instance'\n/*\n * The MODULE, not the barrel, for the two PURE helpers — the specs that render\n * this grid mock `@aglyn/tenant-feature-instance` wholesale to stage their\n * Firestore hooks, and a query builder imported through that barrel disappears\n * under the mock. Neither of these is a hook.\n */\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n LISTING_CATEGORIES,\n listingArtifactType,\n listingArtifactLabel,\n resolvePluginInstallState,\n} from '../model/marketplace'\nimport {\n BROWSE_SORTS,\n type BrowseSort,\n browseBase,\n MARKETPLACE_BROWSE_QUERY,\n} from '../model/listing-query'\nimport { ListingImage } from './listing-image.component'\n\n// The console route table is shared (AGL-685), so these go through\n// buildRoute rather than being reassembled from a base string — the shape\n// of `/[orgSlug]/hosts/[host]/marketplace/…` is not this plugin's to know.\n\n/**\n * How many install records the shelf reads to decide what it already runs.\n *\n * One number for the five collections that answer that question, because the\n * card reads them together and a reader comparing a plugin's chip to a\n * dataset's should not have to know which window was twice the other. It\n * bounds an install-state LOOKUP, not the shelf: the listings query has its\n * own cap, and the grid pages what survives the filters.\n */\nconst INSTALL_STATE_CEILING = 100\n\n/** How the browse clauses read in a refusal. */\nconst BROWSE_HEADERS: Readonly<Record<string, string>> = { category: 'Category' }\n/** How long the search box waits for typing to stop before it asks again. */\nconst SEARCH_SETTLE_MS = 300\n\nexport interface MarketplaceBrowseProps {\n hostId: string\n /** Signed-in user's org permissions, supplied by the shell (AGL-395). */\n permissions?: Partial<OrgPermissions>\n /**\n * Rendered inside the org-scope `/marketplace` route (AGL-772) rather\n * than a site's marketplace tab. Only affects link targets — the grid\n * still installs through the acting `hostId` until targeting lands\n * (AGL-773) — so detail links resolve to the org route, not a per-site\n * one that is being retired.\n */\n orgScoped?: boolean\n /**\n * The acting org's slug from the URL (AGL-867). When given at org scope,\n * detail links build from it directly instead of the async\n * `hostIndex`→`orgs` resolution, which can return empty and leave the detail\n * page — the only place installs happen now — unreachable from browse.\n */\n orgSlug?: string\n /**\n * Restrict the grid to one publisher's listings (AGL-869), for the org-scope\n * publisher page. Omitted on the main browse, which shows everyone.\n */\n publisherId?: string\n}\n\n/**\n * Marketplace components browse (AGL-44). A read-only catalogue: each card links\n * to the listing's detail page, which is the only place an install happens\n * (AGL-867) — installing from a grid card was too easy and skipped the\n * site-targeting choice. The card still shows install STATE (installed,\n * org-wide) so the shelf reads honestly, but carries no install action.\n */\nexport function MarketplaceBrowse(props: MarketplaceBrowseProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { orgScoped, orgSlug: orgSlugProp, publisherId } = props\n const [handles, setHandles] = useState<Record<string, string>>({})\n // Listings are org-owned (AGL-652), so \"is this mine\" is an org comparison.\n // Resolved from the routing mirror rather than a new prop so the component\n // stays self-contained; hostIndex is signed-in readable. `undefined` while\n // it resolves: the shelf's query names the viewer's org in its audience\n // clause, so it waits for the answer rather than asking twice. A failed\n // lookup settles at `null` — the public shelf, without the viewer's own\n // listings in review.\n const [viewerOrgId, setViewerOrgId] = useState<string | null | undefined>(\n undefined,\n )\n useEffect(() => {\n let active = true\n setViewerOrgId(undefined)\n void getDoc(doc(firestore, 'hostIndex', hostId))\n .then((snapshot) => {\n if (active) setViewerOrgId((snapshot.get('orgId') as string) ?? null)\n })\n .catch(() => {\n if (active) setViewerOrgId(null)\n })\n return () => {\n active = false\n }\n }, [firestore, hostId])\n // Card links were built as `/{hostDocId}/marketplace/…`, a shape that has\n // not resolved since AGL-621/622 — every listing and publisher link on\n // this grid 404'd. One shared resolution (AGL-673); null renders plain\n // text rather than a link to nowhere.\n const { orgSlug: resolvedOrgSlug, subdomain } = useConsoleHostRoute(hostId)\n // Prefer the URL-supplied slug (AGL-867) — synchronous and always present at\n // org scope — over the async host resolution, which the per-site route still\n // relies on.\n const orgSlug = orgSlugProp ?? resolvedOrgSlug\n\n // Link targets differ by surface (AGL-772): the org marketplace resolves\n // to the org route (`/[orgSlug]/marketplace/[listingId]`), the per-site tab\n // to the host route. Null → plain text, never a link to nowhere. Publisher\n // pages have no org route yet, so they stay text at org scope for now.\n const listingHref = (listingId: string) =>\n orgScoped\n ? orgSlug\n ? buildRoute(Route.ORG_MARKETPLACE_LISTING, { orgSlug, listingId })\n : undefined\n : orgSlug && subdomain\n ? buildRoute(Route.ORG_MARKETPLACE_LISTING, {\n orgSlug,\n listingId,\n })\n : undefined\n // Storefront links carry the publisher's HANDLE (AGL-1001), the identity\n // shown right beside them on the card. The id remains a valid segment on\n // the page itself, so a publisher whose handle hasn't loaded yet still\n // links somewhere real rather than nowhere.\n const publisherHref = (profileId: string) =>\n orgScoped\n ? // Org-scope publisher storefront (AGL-869): all of one publisher's\n // listings. Needs the URL slug, which is passed in at org scope.\n orgSlug\n ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {\n orgSlug,\n handle: handles[profileId] ?? profileId,\n })\n : undefined\n : orgSlug && subdomain\n ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {\n orgSlug,\n handle: handles[profileId] ?? profileId,\n })\n : undefined\n\n /*\n * THE SHELF IS ITS QUERY (AGL-3321).\n *\n * Browse read the newest ninety listings and resolved everything else in\n * memory — the search box, the category chips, the three sorts, and the\n * gates that keep deleted, private and unreviewed listings off the shelf.\n * A listing that was not among those ninety could not be found by any of\n * them, and \"Most installed\" sorted a sample.\n *\n * Every one of them is on the query now. The gates are one clause, the\n * listing's written `browseAudience` (see `listingBrowseAudience`), asked\n * with `array-contains-any` for everyone (`*`) and the viewer's own org —\n * the owner exemption the in-memory gate carried (AGL-432): a publisher\n * sees their own listing while it waits on review. A publisher page asks\n * the same scopes joined to that publisher. The search folds into the same\n * clause through `browseTokens`, a category is an equality, and each sort\n * is the query's order; `MARKETPLACE_BROWSE_QUERY` declares them and its\n * spec pins the composites that serve them.\n *\n * `browseAudience` is MUTABLE — an unpublish, a takedown or a verdict\n * rewrites it — so a document can stop matching mid-session and be cached\n * as a `noDocument` tombstone at the path the detail page reads by id\n * (AGL-827/929). That was why the `deletedAt` predicate came off this query\n * in AGL-1196; `confirmDisappearances` is the repair that hook documents\n * for a predicate that cannot be dropped, and this one cannot.\n */\n const gridFilter = useListGridFilter({ selectFields: ['category'] })\n const [search, setSearch] = useState('')\n const [searchWords, setSearchWords] = useState<string[]>([])\n useEffect(() => {\n const timer = setTimeout(\n () => setSearchWords(search.split(/\\s+/).filter(Boolean)),\n SEARCH_SETTLE_MS,\n )\n return () => clearTimeout(timer)\n }, [search])\n const [sort, setSort] = useState<BrowseSort>('newest')\n const category =\n gridFilter.clauses.find((clause) => clause.field === 'category')?.value ??\n null\n const listingsRef = useMemo(\n () =>\n viewerOrgId === undefined\n ? null\n : collection(firestore, 'marketplaceListings'),\n [firestore, viewerOrgId],\n )\n const {\n rows: shown,\n status: shelfStatus,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: listingsRef,\n declaration: MARKETPLACE_BROWSE_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: searchWords,\n sort: BROWSE_SORTS[sort],\n base: browseBase(viewerOrgId, publisherId),\n },\n deps: [firestore, viewerOrgId, publisherId],\n idField: '$id',\n confirmDisappearances: true,\n })\n const listings = shown\n const filtering = Boolean(category) || searchWords.length > 0\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: MARKETPLACE_BROWSE_QUERY.fields,\n headers: BROWSE_HEADERS,\n }),\n [plan],\n )\n /*\n * ## The install-state lookups are ORDERED too, and they are not lists\n *\n * The five queries below (this one, the two pin collections, the org\n * datasets and the site's email templates) answer one question per card:\n * has this listing already been installed here. None of them is rendered —\n * each is folded into a map keyed by listing id — so the fix that suits the\n * shelf above does not suit them: there is no page to walk and no footer to\n * put a control in.\n *\n * What they share with it is the defect. A bare `limit` is answered in\n * DOCUMENT-ID order, so an unnamed window is an arbitrary slice of its\n * collection, and a lookup MISS is indistinguishable from a genuine\n * not-installed: a workspace past such a window is shown \"Add to this site\"\n * beside something it already runs, and the detail page — which reads its\n * own pins by id — disagrees with the grid it was reached from.\n *\n * `collectionCeiling` names the order and asks for one row past the\n * ceiling, which is what turns \"the window may have bitten\" into a fact the\n * shelf can state rather than a silence it cannot.\n */\n const { data: installedRead } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'components'),\n INSTALL_STATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: installedDocs, truncated: installedTruncated } = useMemo(\n () => ceilingedWindow<any>(installedRead, INSTALL_STATE_CEILING),\n [installedRead],\n )\n\n // listingId → installed component doc (deleted installs don't count).\n const installed = useMemo(() => {\n const map: Record<string, any> = {}\n for (const definition of installedDocs ?? []) {\n const listingId = definition?.marketplace?.listingId\n if (listingId && !definition.deletedAt) map[listingId] = definition\n }\n return map\n }, [installedDocs])\n\n // Plugin installs are version PINS, not component snapshots (AGL-656): the\n // `components` map above never holds one, so a plugin already installed —\n // at host or org scope — showed \"Add to this site\". These two pin\n // collections are what the loader honors (a host pin shadows an org pin).\n const orgId = useHostOrgId(hostId)\n const { data: hostPinRead } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'installs'),\n INSTALL_STATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: hostPinDocs, truncated: hostPinsTruncated } = useMemo(\n () => ceilingedWindow<any>(hostPinRead, INSTALL_STATE_CEILING),\n [hostPinRead],\n )\n // Held at null while `useHostOrgId` is in flight, never `orgs/-pending-`\n // (AGL-1440): the AGL-1047 comment seven lines below records the same\n // denial-every-mount shape for `useScopeTokens` — this listen had it too.\n const { data: orgPinRead } = useFirestoreCollection<any>(\n () =>\n orgId\n ? collectionCeiling(\n collection(firestore, 'orgs', orgId, 'installs'),\n INSTALL_STATE_CEILING,\n )\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n const { rows: orgPinDocs, truncated: orgPinsTruncated } = useMemo(\n () => ceilingedWindow<any>(orgPinRead, INSTALL_STATE_CEILING),\n [orgPinRead],\n )\n // Scoped (AGL-1044): an unfiltered list is REJECTED for a scoped member,\n // not filtered, so this would error without the constraint.\n //\n // Wait for the member doc before listing (AGL-1047). `useScopeTokens`\n // reports `orgWide: true` while loading, so without `scopeLoaded` a scoped\n // collaborator's first render computes `needsScope: false` and sends an\n // UNFILTERED list that the AGL-1041 rules deny per document. It recovers\n // on the next render, which is what makes it easy to miss: the page looks\n // right and logs a denial every mount.\n const {\n tokens: scopeTokens,\n orgWide: viewerOrgWide,\n loaded: scopeLoaded,\n } = useScopeTokens(orgId ?? undefined)\n const needsScope = Boolean(orgId) && !viewerOrgWide\n // The AGL-657 types land in neither the components collection nor a pin\n // (AGL-789): a dataset schema becomes an org dataset, an email template a\n // draft version. Both installers stamp the source listing, so read those.\n /*\n * `documentId()` rather than a field, for the reason the audience sweep in\n * `campaign-send.ts` gives: Firestore's automatic single-field index for an\n * array member is keyed on the value and the document name, so\n * `array-contains-any` plus `orderBy(__name__)` is served by it. Ordering on\n * anything else would need a composite index per scope shape.\n *\n * The ceiling is the shared one. It is not sized for the dataset\n * collection: it is sized for the number of LISTINGS whose install state a\n * shelf of ninety cards can describe, which is the same question its four\n * neighbours answer, so it is the same number.\n */\n const { data: datasetRead } = useFirestoreCollection<any>(\n () =>\n orgId && scopeLoaded\n ? query(\n collection(firestore, 'orgs', orgId, 'datasets'),\n ...(needsScope\n ? [where('visibleTo', 'array-contains-any', scopeTokens)]\n : []),\n orderBy(documentId()),\n limit(INSTALL_STATE_CEILING + 1),\n )\n : null,\n [firestore, orgId, scopeLoaded, needsScope, scopeTokens],\n // `visibleTo` is MUTABLE — every scope edit rewrites it — so this query\n // can tombstone a dataset the way AGL-827 tombstoned a host. The rule\n // requires the constraint for anyone who is not org-wide, so unlike the\n // listings query above the predicate cannot simply be dropped (AGL-1196).\n { idField: '$id', confirmDisappearances: true },\n )\n const { rows: datasetDocs, truncated: datasetsTruncated } = useMemo(\n () => ceilingedWindow<any>(datasetRead, INSTALL_STATE_CEILING),\n [datasetRead],\n )\n const { data: emailRead } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'emailTemplates'),\n INSTALL_STATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: emailDocs, truncated: emailsTruncated } = useMemo(\n () => ceilingedWindow<any>(emailRead, INSTALL_STATE_CEILING),\n [emailRead],\n )\n // A theme is not a document in a collection — it is a field on the site\n // (AGL-1020) — so its install is read from the host doc rather than from a\n // per-artifact query like the two above.\n const { data: hostDoc } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n // listingId → the newest install of it. A schema install deliberately makes\n // a NEW dataset every time, so this is genuinely one-to-many; the highest\n // installed version is what the card should speak for.\n const artifactInstalls = useMemo(() => {\n const map: Record<string, { version: string | null }> = {}\n const note = (listingId: unknown, version: unknown) => {\n const id = listingId ? String(listingId) : ''\n if (!id) return\n const next = version != null ? String(version) : null\n const seen = map[id]?.version\n if (!seen || (next && next > seen)) map[id] = { version: next }\n }\n for (const dataset of datasetDocs ?? []) {\n if (dataset.deletedAt) continue\n note(dataset.source?.listingId, dataset.source?.version)\n }\n for (const template of emailDocs ?? []) {\n if (template.deletedAt) continue\n note(template.installedFrom?.listingId, template.installedFrom?.version)\n }\n note(hostDoc?.themeInstalledFrom?.listingId, hostDoc?.themeInstalledFrom?.version)\n return map\n }, [datasetDocs, emailDocs, hostDoc])\n\n const hostPins = useMemo(() => {\n const map: Record<string, any> = {}\n for (const pin of hostPinDocs ?? []) map[pin.$id] = pin\n return map\n }, [hostPinDocs])\n const orgPins = useMemo(() => {\n const map: Record<string, any> = {}\n for (const pin of orgPinDocs ?? []) map[pin.$id] = pin\n return map\n }, [orgPinDocs])\n\n // Resolve each listing's publisher handle once. `handles` must NOT be a\n // dependency here: the effect writes `handles`, so listing it would make the\n // effect re-run on its own output. During the post-load window, when\n // `listings` arrives while every other subscription on this always-mounted\n // grid is also settling, that self-retrigger adds render+effect cycles to an\n // already dense flurry — enough that a concurrent update elsewhere can trip\n // React's nested-update limit (AGL-785). A ref of already-requested ids\n // dedupes instead, so the effect depends only on `listings`.\n const requestedHandles = useRef<Set<string>>(new Set())\n useEffect(() => {\n const profileIds = [\n ...new Set((listings ?? []).map((listing: any) => listing.profileId)),\n ].filter(\n (profileId) =>\n profileId && !requestedHandles.current.has(String(profileId)),\n )\n if (!profileIds.length) return\n for (const profileId of profileIds)\n requestedHandles.current.add(String(profileId))\n let cancelled = false\n Promise.all(\n profileIds.map(async (profileId) => {\n const snapshot = await getDoc(\n doc(firestore, 'publisherProfiles', String(profileId)),\n ).catch(() => null)\n return [profileId, snapshot?.get('handle') ?? ''] as const\n }),\n ).then((entries) => {\n if (!cancelled) {\n setHandles((prev) => ({\n ...prev,\n ...Object.fromEntries(entries),\n }))\n }\n })\n return () => {\n cancelled = true\n }\n }, [listings, firestore])\n\n /**\n * The install-state lookups did not read everything they were asked about.\n *\n * Said on the SHELF rather than on the card that is wrong, because there is\n * no way to know which card that is: a miss and a genuine not-installed are\n * the same absence from the same map. What the reader can act on is that the\n * chips are a floor — anything marked installed is installed, and something\n * unmarked may still be.\n */\n const installStateTruncated =\n installedTruncated ||\n hostPinsTruncated ||\n orgPinsTruncated ||\n datasetsTruncated ||\n emailsTruncated\n\n return (\n <CardDisplay\n header={'Marketplace components'}\n help={pluginDocsHelp('plugins', { anchor: '#install--upgrade' })}\n contentGutterX\n contentGutterY\n >\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ mb: 2, alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <TextField\n placeholder=\"Search components…\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n size=\"small\"\n sx={{ minWidth: 200 }}\n />\n {/* The fixed taxonomy (AGL-430), not the categories the loaded page\n happened to hold: a chip is a question for the query, and one\n missing because no listing on this page had it could never be\n asked. */}\n {LISTING_CATEGORIES.map((value) => (\n <Chip\n key={value}\n label={value}\n variant={category === value ? 'filled' : 'outlined'}\n color={category === value ? 'primary' : 'default'}\n onClick={() =>\n gridFilter.setClauses(\n category === value\n ? []\n : [{ field: 'category', op: 'equals', value }],\n )\n }\n />\n ))}\n <TextField\n value={sort}\n onChange={(event) => setSort(event.target.value as BrowseSort)}\n size=\"small\"\n select\n sx={{ ml: 'auto', minWidth: 150 }}\n >\n <MenuItem value=\"newest\">{'Newest'}</MenuItem>\n <MenuItem value=\"installed\">{'Most installed'}</MenuItem>\n <MenuItem value=\"rated\">{'Highest rated'}</MenuItem>\n </TextField>\n </Stack>\n {installStateTruncated ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {`Installed state is resolved from the first ${INSTALL_STATE_CEILING} ` +\n 'install records in this workspace, ordered by id. There are ' +\n 'more, so a card may not be marked installed even though it is. ' +\n 'Open a listing to see what this workspace actually runs.'}\n </Alert>\n ) : null}\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n {shown.length === 0 && page === 0 ? (\n shelfStatus === 'success' ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {filtering\n ? 'No components match this search.'\n : 'No marketplace components published yet — publish one of ' +\n 'your reusable components from the Setup page to be the first.'}\n </Typography>\n ) : null\n ) : (\n <>\n <Grid container spacing={2}>\n {shown.map((listing: any) => {\n const artifactType = listingArtifactType(listing)\n const isPlugin = artifactType === 'plugin'\n const pluginState = resolvePluginInstallState(\n listing.latestVersion,\n isPlugin ? hostPins[listing.$id] : null,\n isPlugin ? orgPins[listing.$id] : null,\n )\n const componentInstall = installed[listing.$id]\n // datasetSchema/emailTemplate installs are tracked by the source\n // listing stamped on what they created (AGL-789).\n //\n // `emailStarter` is deliberately absent. Every other type has ONE\n // install per site to point at — a pin, a dataset, a draft on a\n // fixed catalog key, the site's theme — so \"Installed\" names\n // something. A starter installs as a NEW email each time, and a\n // site may keep five of them; there is no single copy for a chip\n // to describe, and marking the listing installed would grey out\n // the button that makes the sixth.\n const artifactInstall =\n artifactType === 'datasetSchema' ||\n artifactType === 'emailTemplate' ||\n artifactType === 'theme'\n ? artifactInstalls[listing.$id]\n : undefined\n const isInstalled = isPlugin\n ? pluginState.scope != null\n : Boolean(componentInstall ?? artifactInstall)\n const installedVersion = isPlugin\n ? pluginState.installedVersion\n : (componentInstall?.marketplace?.version ??\n artifactInstall?.version)\n const priceUsd = Number(listing.priceUsd ?? 0)\n const detailHref = listingHref(listing.$id)\n return (\n <Grid key={listing.$id} size={{ xs: 12, sm: 6, md: 4 }}>\n <Stack\n spacing={1}\n sx={{\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n p: 2,\n height: '100%',\n }}\n >\n {/* Resolved, never raw (AGL-1424) — the stored value may be\n a `media:` reference. */}\n <ListingImage\n src={listing.previewImageUrl}\n alt={`${listing.displayName} preview`}\n sx={{\n width: '100%',\n height: 120,\n objectFit: 'cover',\n borderRadius: 1,\n }}\n />\n {/* The name gets the whole line (AGL-1002). Sharing a row\n with the chips meant the title was the only thing that\n gave way when they did not fit — \"Promo Countdown\"\n truncated to \"Promo Coun…\" beside chips with room to\n spare, which inverts what matters on a browse card. */}\n <AppLink\n href={listingHref(listing.$id)}\n color=\"inherit\"\n underline=\"hover\"\n variant=\"subtitle2\"\n >\n {listing.displayName}\n </AppLink>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n {/* Primary classification, said first (AGL-864). */}\n <Chip\n size=\"small\"\n color=\"primary\"\n label={listingArtifactLabel(listing)}\n />\n {listing.category ? (\n <Chip size=\"small\" label={listing.category} />\n ) : null}\n {/* The reviewed badge belongs HERE most of all\n (AGL-1002): the detail page showed it, but browse is\n where someone is comparing options and deciding whose\n code to run.\n\n TWO CLAIMS, SHOWN SEPARATELY (AGL-1121). This chip\n vouches for the PUBLISHER; the one beside it says\n whether THESE bytes were reviewed. They were a single\n \"Verified\" chip driven by `reviewStatus`, which lives\n on the listing and deliberately survives a version\n bump — so a publisher verified on v1.0.0 could ship\n v1.9.0 containing anything and still be badged as\n though the code had been read.\n\n Both tooltips name `PLATFORM_BRAND_NAME` rather than\n the viewing org's `productName` (AGL-2351). They say\n who did the verifying and the reading — the\n deployment's own review queue, one catalog shared by\n every org — and a white-label agency's brand here would\n claim the agency vetted a publisher it has never heard\n of. */}\n {listing.reviewStatus === 'verified' ? (\n <Tooltip\n title={\n `A human at ${PLATFORM_BRAND_NAME} confirmed who ` +\n 'this publisher is, ' +\n 'and that the listing describes what the code does. ' +\n 'It is a claim about the publisher, not about this ' +\n 'release — it survives a version bump.'\n }\n >\n <Chip\n size=\"small\"\n color=\"info\"\n label=\"Verified publisher\"\n // Carries more weight than the neighbouring\n // classification chips on purpose: it is the only one\n // that says a human vouched for the code, and the\n // rest are just taxonomy. The icon is what makes it\n // findable when scanning a grid rather than reading\n // one card.\n icon={\n <MdiIcon\n path={mdiCheckDecagram.path}\n sx={{ fontSize: 16 }}\n />\n }\n sx={{ fontWeight: 600 }}\n />\n </Tooltip>\n ) : null}\n {/* The bytes on offer, not the person who wrote them.\n Absent on a listing published before this field\n existed, which reads as \"not reviewed\" — the safe\n direction for a claim about code. */}\n {listing.latestVersionReviewState === 'approved' ? (\n <Tooltip\n title={\n `A human at ${PLATFORM_BRAND_NAME} read these exact ` +\n 'bytes — the ' +\n 'version on offer — against a required checklist. ' +\n 'Re-earned per version, so a new release starts ' +\n 'without it. Not a security guarantee: every plugin ' +\n 'runs in the same sandbox either way.'\n }\n >\n <Chip size=\"small\" color=\"success\" label=\"Reviewed\" />\n </Tooltip>\n ) : null}\n {/*\n Price on EVERY card, `Free` included (AGL-2173). The\n chip only rendered above zero, so the four free\n listings the marketplace mockup shows carried no\n price at all — and \"no chip\" is the one reading a\n shopper cannot distinguish from \"we forgot\", on the\n row where the paid listing beside it says $29.\n */}\n <Chip\n size=\"small\"\n color={priceUsd > 0 ? 'primary' : 'success'}\n variant={priceUsd > 0 ? 'filled' : 'outlined'}\n label={priceUsd > 0 ? `$${priceUsd}` : 'Free'}\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`v${listing.latestVersion}`}\n {handles[listing.profileId] ? (\n <>\n {' · by '}\n <AppLink\n href={publisherHref(listing.profileId)}\n color=\"primary\"\n underline=\"hover\"\n >\n {`@${handles[listing.profileId]}`}\n </AppLink>\n </>\n ) : (\n ''\n )}\n {listing.installCount\n ? ` · ${listing.installCount} install${\n listing.installCount === 1 ? '' : 's'\n }`\n : ''}\n </Typography>\n {/*\n Stars, as the mockup draws them (AGL-2173). The count\n stays alongside the average: \"5.0\" from one rating and\n from forty are not the same claim (AGL-655). An unrated\n listing says so rather than rendering nothing — silence\n read as \"no stars\", which is the opposite of \"not yet\n rated\".\n */}\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n {listing.ratingCount ? (\n <>\n <Rating\n size=\"small\"\n readOnly\n precision={0.1}\n value={Number(listing.ratingAverage ?? 0)}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${listing.ratingAverage ?? 0} (${listing.ratingCount})`}\n </Typography>\n </>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not yet rated'}\n </Typography>\n )}\n </Stack>\n {listing.description ? (\n <Typography\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ flex: 1 }}\n // Four lines, then ellipsis (AGL-1002). Grid rows\n // stretch to their tallest card, so one publisher's\n // essay used to leave every card beside it mostly white\n // space; the full text is one click away.\n //\n // Plain `style`, not `sx`: emotion drops the\n // `-webkit-box` display value on the way through, and\n // without it the other three properties clamp nothing\n // (measured — `display` computed to `flow-root` while\n // line-clamp and box-orient came through fine). Nothing\n // here reads the theme, so there is nothing to lose.\n style={{\n display: '-webkit-box',\n WebkitBoxOrient: 'vertical',\n WebkitLineClamp: 4,\n overflow: 'hidden',\n }}\n >\n {listing.description}\n </Typography>\n ) : (\n <span style={{ flex: 1 }} />\n )}\n {/* Read-only state, then a link to the detail page — the\n only place an install happens (AGL-867). No install/buy\n action lives on the grid. */}\n {/* Install state reads as one statement (AGL-1002): the\n scope belongs with \"Installed\", not up in the chip row\n among the type and category pills, which say what the\n listing IS rather than what this workspace has done\n with it. Org-wide installs apply to every site\n (AGL-656). */}\n {isInstalled ? (\n <Stack\n direction=\"row\"\n spacing={0.75}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Typography variant=\"caption\" color=\"success.main\">\n {`Installed${\n installedVersion ? ` (v${installedVersion})` : ''\n }`}\n </Typography>\n {isPlugin && pluginState.scope === 'org' ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n pluginState.shadowed\n ? 'Org-wide (shadowed)'\n : 'Org-wide'\n }\n />\n ) : null}\n </Stack>\n ) : null}\n <AppLink\n componentVariant=\"button\"\n size=\"small\"\n variant=\"outlined\"\n color=\"primary\"\n href={detailHref ?? ''}\n disabled={!detailHref}\n >\n {priceUsd > 0 && !isInstalled\n ? `View details · $${priceUsd}`\n : 'View details'}\n </AppLink>\n </Stack>\n </Grid>\n )\n })}\n </Grid>\n {/* The query pages the shelf, so the total is not known — only\n whether another page exists. */}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </CardDisplay>\n )\n}\nMarketplaceBrowse.displayName = 'MarketplaceBrowse'\n\nexport default MarketplaceBrowse\n"],"names":["mdiCheckDecagram","AppLink","CardDisplay","MdiIcon","ListPagination","ListQueryNotices","listQueryRefusals","useListGridFilter","Alert","Chip","Tooltip","Grid","MenuItem","Rating","Stack","TextField","Typography","collection","doc","documentId","getDoc","limit","orderBy","query","where","useListQuery","useEffect","useMemo","useRef","useState","buildRoute","PLATFORM_BRAND_NAME","pluginDocsHelp","Route","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","useHostOrgId","useScopeTokens","ceilingedWindow","collectionCeiling","LISTING_CATEGORIES","listingArtifactType","listingArtifactLabel","resolvePluginInstallState","BROWSE_SORTS","browseBase","MARKETPLACE_BROWSE_QUERY","ListingImage","INSTALL_STATE_CEILING","BROWSE_HEADERS","category","SEARCH_SETTLE_MS","MarketplaceBrowse","props","gridFilter","hostId","firestore","orgScoped","orgSlug","orgSlugProp","publisherId","handles","setHandles","viewerOrgId","setViewerOrgId","undefined","active","then","snapshot","get","catch","resolvedOrgSlug","subdomain","listingHref","listingId","ORG_MARKETPLACE_LISTING","publisherHref","profileId","ORG_MARKETPLACE_PUBLISHER","handle","selectFields","search","setSearch","searchWords","setSearchWords","timer","setTimeout","split","filter","Boolean","clearTimeout","sort","setSort","clauses","find","clause","field","value","listingsRef","rows","shown","status","shelfStatus","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","base","deps","idField","confirmDisappearances","listings","filtering","length","refusals","refused","fields","headers","data","installedRead","installedDocs","truncated","installedTruncated","installed","map","definition","marketplace","deletedAt","orgId","hostPinRead","hostPinDocs","hostPinsTruncated","orgPinRead","orgPinDocs","orgPinsTruncated","tokens","scopeTokens","orgWide","viewerOrgWide","loaded","scopeLoaded","needsScope","datasetRead","datasetDocs","datasetsTruncated","emailRead","emailDocs","emailsTruncated","hostDoc","artifactInstalls","note","version","id","String","next","seen","dataset","source","template","installedFrom","themeInstalledFrom","hostPins","pin","$id","orgPins","requestedHandles","Set","profileIds","listing","current","has","add","cancelled","Promise","all","entries","prev","Object","fromEntries","installStateTruncated","header","help","anchor","contentGutterX","contentGutterY","direction","spacing","sx","mb","alignItems","flexWrap","rowGap","placeholder","onChange","event","target","size","minWidth","label","variant","color","onClick","setClauses","op","select","ml","severity","notices","container","componentInstall","artifactType","isPlugin","pluginState","latestVersion","artifactInstall","isInstalled","scope","installedVersion","priceUsd","Number","detailHref","xs","sm","md","border","borderColor","borderRadius","p","height","src","previewImageUrl","alt","displayName","width","objectFit","href","underline","reviewStatus","title","icon","path","fontSize","fontWeight","latestVersionReviewState","installCount","ratingCount","readOnly","precision","ratingAverage","description","flex","style","display","WebkitBoxOrient","WebkitLineClamp","overflow","span","shadowed","componentVariant","disabled","rowCount","onPageChange","onPageSizeChange"],"mappings":"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"}
@@ -52,6 +52,7 @@ const when = (ms)=>ms ? new Date(ms).toLocaleString() : '—';
52
52
  const { enqueueSnackbar } = useSnackbar();
53
53
  const [reports, setReports] = useState(null);
54
54
  const [identityVisible, setIdentityVisible] = useState(false);
55
+ /** The status holds more reports than the route read (AGL-3321). */ const [more, setMore] = useState(false);
55
56
  const [statusFilter, setStatusFilter] = useState('open');
56
57
  const [error, setError] = useState(null);
57
58
  const [loading, setLoading] = useState(false);
@@ -82,6 +83,7 @@ const when = (ms)=>ms ? new Date(ms).toLocaleString() : '—';
82
83
  } else {
83
84
  var _ref1;
84
85
  setReports((_ref1 = body == null ? void 0 : body.reports) != null ? _ref1 : []);
86
+ setMore(Boolean(body == null ? void 0 : body.more));
85
87
  setIdentityVisible(Boolean(body == null ? void 0 : body.identityVisible));
86
88
  }
87
89
  } catch (unused) {
@@ -198,6 +200,13 @@ const when = (ms)=>ms ? new Date(ms).toLocaleString() : '—';
198
200
  })
199
201
  },
200
202
  children: [
203
+ more ? /*#__PURE__*/ _jsx(Alert, {
204
+ severity: "info",
205
+ sx: {
206
+ mb: 2
207
+ },
208
+ children: 'These are the most recently updated reports in this ' + 'state, and there are older ones not shown. Moving a ' + 'report on takes it out of this list.'
209
+ }) : null,
201
210
  !identityVisible ? /*#__PURE__*/ _jsx(Alert, {
202
211
  severity: "info",
203
212
  sx: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/marketplace-reports-page.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { ICON_VARIANT_SYMBOL_FLAG } from '@aglyn/shared-data-enums'\nimport { PageHeaderHelp, pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n LinearProgress,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\ninterface MarketplaceReportRow {\n id: string\n status: string\n targetType: string\n listingId: string | null\n listingName: string | null\n publisherOrgId: string | null\n reviewUid: string | null\n reason: string | null\n reporterKnown: boolean\n reporterUid: string | null\n resolution: string | null\n resolvedByEmail: string | null\n createdAtMs: number | null\n updatedAtMs: number | null\n}\n\nconst STATUS_COLOR: Record<string, 'warning' | 'info' | 'success' | 'default'> =\n {\n open: 'warning',\n reviewing: 'info',\n actioned: 'success',\n dismissed: 'default',\n }\n\nconst when = (ms: number | null) =>\n ms ? new Date(ms).toLocaleString() : '—'\n\n/**\n * The marketplace report queue (AGL-2310).\n *\n * The report button on a listing has told users their report was filed since\n * it shipped. `marketplaceReports` was written and read by NOTHING — no\n * surface, no cron, no collection group — so every report was stored,\n * acknowledged and unreachable, and `status` never moved off `open` because\n * no queue displayed it.\n *\n * The page is deliberately the `abuseReports` queue in miniature: a list, a\n * status transition, a note required to close, and an `adminAudit` row per\n * transition. Two vocabularies for the same act is how one of them stops\n * being maintained.\n *\n * The REASON is the row. Everything else — the listing, the publisher, the\n * date — is context for it, which is why it is rendered in full rather than\n * truncated into a column, and why it is never redacted by staff tier.\n */\nexport function MarketplaceReportsPage() {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [reports, setReports] = useState<MarketplaceReportRow[] | null>(null)\n const [identityVisible, setIdentityVisible] = useState(false)\n const [statusFilter, setStatusFilter] = useState('open')\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(false)\n const [busyId, setBusyId] = useState<string | null>(null)\n const [notes, setNotes] = useState<Record<string, string>>({})\n const [epoch, setEpoch] = useState(0)\n\n useEffect(() => {\n let active = true\n void (async () => {\n /*\n * The staff claim is the SHELL's now (AGL-3080). This page used to\n * hold `isStaff !== true` here, because it owned its own route and a\n * request fired from the claim's loading `null` answers a question\n * nobody asked. The generic staff route renders every plugin page\n * inside `StaffOnly`, which renders NOTHING while the claim resolves\n * and 404s a non-holder — so being mounted at all is the claim, and a\n * second check here would be a copy free to disagree with it.\n */\n if (!user) return\n setLoading(true)\n setError(null)\n try {\n const response = await authorizedFetch(\n user,\n `/api/marketplace/admin/reports?status=${encodeURIComponent(statusFilter)}`,\n )\n const body = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok) {\n setError(body?.error ?? 'Report lookup failed')\n } else {\n setReports((body?.reports ?? []) as MarketplaceReportRow[])\n setIdentityVisible(Boolean(body?.identityVisible))\n }\n } catch {\n if (active) setError('Report lookup failed')\n } finally {\n if (active) setLoading(false)\n }\n })()\n return () => {\n active = false\n }\n }, [user, statusFilter, epoch])\n\n const transition = useCallback(\n async (report: MarketplaceReportRow, status: string) => {\n setBusyId(report.id)\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/admin/reports',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: report.id,\n status,\n resolution: notes[report.id] ?? '',\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // The server's own sentence, not a generic failure: the one refusal\n // this route issues (\"say what you did\") is instruction, and\n // replacing it with \"Update failed\" would leave the reviewer\n // guessing at a rule the server already stated.\n enqueueSnackbar(payload?.error ?? 'Update failed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n return\n }\n enqueueSnackbar(`Marked ${status}`, { variant: 'success', persist: false })\n setEpoch((value) => value + 1)\n } catch {\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusyId(null)\n }\n },\n [user, notes, enqueueSnackbar],\n )\n\n return (\n <>\n {/*\n The abuse-queue topic, deliberately: this is the same triage job on a\n different surface, and a second docs page describing the same four\n statuses is a second page to forget to update.\n\n ANCHORED, though, and published rather than declared. The abuse\n queue's own page opens the same topic bare, and two help icons landing\n in exactly the same place are interchangeable — which is the thing\n `docs-help-destinations` exists to catch. A `staffPages` entry carries\n one `docsTopic` and no anchor, so `#statuses` — the four states and\n what closing one means, which is what a reader arriving from HERE\n needs — comes up through the header seam instead.\n */}\n <PageHeaderHelp topic=\"abuseReports\" anchor=\"#statuses\" />\n <Stack spacing={3}>\n {loading && <LinearProgress />}\n {error && <Alert severity=\"error\">{error}</Alert>}\n <CardDisplay\n header={'Reported listings and reviews'}\n help={pluginDocsHelp('abuseReports', {\n excerpt:\n 'Reports users filed against a marketplace listing or a ' +\n 'review, with the reason each one gave. Closing one needs ' +\n 'a note, and every change is audited.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <TextField\n select\n size=\"small\"\n value={statusFilter}\n onChange={(event) => {\n setReports(null)\n setStatusFilter(event.target.value)\n }}\n >\n <MenuItem value=\"open\">{'Open'}</MenuItem>\n <MenuItem value=\"reviewing\">{'Reviewing'}</MenuItem>\n <MenuItem value=\"actioned\">{'Actioned'}</MenuItem>\n <MenuItem value=\"dismissed\">{'Dismissed'}</MenuItem>\n <MenuItem value=\"\">{'Everything'}</MenuItem>\n </TextField>\n ),\n }}\n >\n {!identityVisible ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {'Reporter accounts are hidden at your access level. Every ' +\n 'report still shows whether there is somebody behind it.'}\n </Alert>\n ) : null}\n {reports === null ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {loading ? 'Reading reports…' : ''}\n </Typography>\n ) : reports.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Nothing in this state.'}\n </Typography>\n ) : (\n <Stack spacing={3}>\n {reports.map((report) => (\n <Stack key={report.id} spacing={1}>\n <Stack\n direction=\"row\"\n sx={{ flexWrap: 'wrap', gap: 1, alignItems: 'center' }}\n >\n <Chip\n size=\"small\"\n color={STATUS_COLOR[report.status] ?? 'default'}\n label={report.status}\n />\n <Chip size=\"small\" variant=\"outlined\" label={report.targetType} />\n <Typography variant=\"subtitle2\">\n {report.listingName || report.listingId || report.id}\n </Typography>\n </Stack>\n {/* THE REASON, in full. This is the whole content of a\n report; a queue that summarised it would be the\n original silence with an extra click in it. */}\n <Typography variant=\"body2\">\n {report.reason || '(no reason given)'}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n report.publisherOrgId\n ? `publisher ${report.publisherOrgId}`\n : null,\n // A REDACTED reporter and an ANONYMOUS one are\n // different facts: only the second means there is\n // nobody to follow up with.\n report.reporterUid\n ? `reporter ${report.reporterUid}`\n : report.reporterKnown\n ? 'reporter hidden'\n : 'no reporter recorded',\n `filed ${when(report.createdAtMs)}`,\n report.resolvedByEmail\n ? `closed by ${report.resolvedByEmail}`\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n {report.resolution ? (\n <Alert severity=\"success\">{report.resolution}</Alert>\n ) : null}\n <Stack\n direction=\"row\"\n sx={{ flexWrap: 'wrap', gap: 1, alignItems: 'center' }}\n >\n <TextField\n size=\"small\"\n placeholder=\"What did you do, and why?\"\n value={notes[report.id] ?? ''}\n onChange={(event) =>\n setNotes((previous) => ({\n ...previous,\n [report.id]: event.target.value,\n }))\n }\n sx={{ flex: 1, minWidth: 260 }}\n />\n <Button\n size=\"small\"\n disabled={busyId === report.id}\n onClick={() => void transition(report, 'reviewing')}\n >\n {'Reviewing'}\n </Button>\n <Button\n size=\"small\"\n color=\"success\"\n disabled={busyId === report.id}\n onClick={() => void transition(report, 'actioned')}\n >\n {'Actioned'}\n </Button>\n <Button\n size=\"small\"\n color=\"warning\"\n disabled={busyId === report.id}\n onClick={() => void transition(report, 'dismissed')}\n >\n {'Dismiss'}\n </Button>\n </Stack>\n </Stack>\n ))}\n </Stack>\n )}\n </CardDisplay>\n </Stack>\n </>\n )\n}\nMarketplaceReportsPage.displayName = 'MarketplaceReportsPage'\n\nexport default MarketplaceReportsPage\n"],"names":["PageHeaderHelp","pluginDocsHelp","CardDisplay","useSnackbar","Alert","Button","Chip","LinearProgress","MenuItem","Stack","TextField","Typography","useCallback","useEffect","useState","useUser","authorizedFetch","STATUS_COLOR","open","reviewing","actioned","dismissed","when","ms","Date","toLocaleString","MarketplaceReportsPage","data","user","enqueueSnackbar","reports","setReports","identityVisible","setIdentityVisible","statusFilter","setStatusFilter","error","setError","loading","setLoading","busyId","setBusyId","notes","setNotes","epoch","setEpoch","active","response","encodeURIComponent","body","json","catch","ok","Boolean","transition","report","status","id","method","headers","JSON","stringify","resolution","payload","variant","allowDuplicate","persist","value","topic","anchor","spacing","severity","header","help","excerpt","contentGutterX","contentGutterY","HeaderProps","action","select","size","onChange","event","target","sx","mb","color","length","map","direction","flexWrap","gap","alignItems","label","targetType","listingName","listingId","reason","publisherOrgId","reporterUid","reporterKnown","createdAtMs","resolvedByEmail","filter","join","placeholder","previous","flex","minWidth","disabled","onClick","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/marketplace-reports-page.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { ICON_VARIANT_SYMBOL_FLAG } from '@aglyn/shared-data-enums'\nimport { PageHeaderHelp, pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n LinearProgress,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\ninterface MarketplaceReportRow {\n id: string\n status: string\n targetType: string\n listingId: string | null\n listingName: string | null\n publisherOrgId: string | null\n reviewUid: string | null\n reason: string | null\n reporterKnown: boolean\n reporterUid: string | null\n resolution: string | null\n resolvedByEmail: string | null\n createdAtMs: number | null\n updatedAtMs: number | null\n}\n\nconst STATUS_COLOR: Record<string, 'warning' | 'info' | 'success' | 'default'> =\n {\n open: 'warning',\n reviewing: 'info',\n actioned: 'success',\n dismissed: 'default',\n }\n\nconst when = (ms: number | null) =>\n ms ? new Date(ms).toLocaleString() : '—'\n\n/**\n * The marketplace report queue (AGL-2310).\n *\n * The report button on a listing has told users their report was filed since\n * it shipped. `marketplaceReports` was written and read by NOTHING — no\n * surface, no cron, no collection group — so every report was stored,\n * acknowledged and unreachable, and `status` never moved off `open` because\n * no queue displayed it.\n *\n * The page is deliberately the `abuseReports` queue in miniature: a list, a\n * status transition, a note required to close, and an `adminAudit` row per\n * transition. Two vocabularies for the same act is how one of them stops\n * being maintained.\n *\n * The REASON is the row. Everything else — the listing, the publisher, the\n * date — is context for it, which is why it is rendered in full rather than\n * truncated into a column, and why it is never redacted by staff tier.\n */\nexport function MarketplaceReportsPage() {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [reports, setReports] = useState<MarketplaceReportRow[] | null>(null)\n const [identityVisible, setIdentityVisible] = useState(false)\n /** The status holds more reports than the route read (AGL-3321). */\n const [more, setMore] = useState(false)\n const [statusFilter, setStatusFilter] = useState('open')\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(false)\n const [busyId, setBusyId] = useState<string | null>(null)\n const [notes, setNotes] = useState<Record<string, string>>({})\n const [epoch, setEpoch] = useState(0)\n\n useEffect(() => {\n let active = true\n void (async () => {\n /*\n * The staff claim is the SHELL's now (AGL-3080). This page used to\n * hold `isStaff !== true` here, because it owned its own route and a\n * request fired from the claim's loading `null` answers a question\n * nobody asked. The generic staff route renders every plugin page\n * inside `StaffOnly`, which renders NOTHING while the claim resolves\n * and 404s a non-holder — so being mounted at all is the claim, and a\n * second check here would be a copy free to disagree with it.\n */\n if (!user) return\n setLoading(true)\n setError(null)\n try {\n const response = await authorizedFetch(\n user,\n `/api/marketplace/admin/reports?status=${encodeURIComponent(statusFilter)}`,\n )\n const body = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok) {\n setError(body?.error ?? 'Report lookup failed')\n } else {\n setReports((body?.reports ?? []) as MarketplaceReportRow[])\n setMore(Boolean(body?.more))\n setIdentityVisible(Boolean(body?.identityVisible))\n }\n } catch {\n if (active) setError('Report lookup failed')\n } finally {\n if (active) setLoading(false)\n }\n })()\n return () => {\n active = false\n }\n }, [user, statusFilter, epoch])\n\n const transition = useCallback(\n async (report: MarketplaceReportRow, status: string) => {\n setBusyId(report.id)\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/admin/reports',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: report.id,\n status,\n resolution: notes[report.id] ?? '',\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // The server's own sentence, not a generic failure: the one refusal\n // this route issues (\"say what you did\") is instruction, and\n // replacing it with \"Update failed\" would leave the reviewer\n // guessing at a rule the server already stated.\n enqueueSnackbar(payload?.error ?? 'Update failed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n return\n }\n enqueueSnackbar(`Marked ${status}`, { variant: 'success', persist: false })\n setEpoch((value) => value + 1)\n } catch {\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusyId(null)\n }\n },\n [user, notes, enqueueSnackbar],\n )\n\n return (\n <>\n {/*\n The abuse-queue topic, deliberately: this is the same triage job on a\n different surface, and a second docs page describing the same four\n statuses is a second page to forget to update.\n\n ANCHORED, though, and published rather than declared. The abuse\n queue's own page opens the same topic bare, and two help icons landing\n in exactly the same place are interchangeable — which is the thing\n `docs-help-destinations` exists to catch. A `staffPages` entry carries\n one `docsTopic` and no anchor, so `#statuses` — the four states and\n what closing one means, which is what a reader arriving from HERE\n needs — comes up through the header seam instead.\n */}\n <PageHeaderHelp topic=\"abuseReports\" anchor=\"#statuses\" />\n <Stack spacing={3}>\n {loading && <LinearProgress />}\n {error && <Alert severity=\"error\">{error}</Alert>}\n <CardDisplay\n header={'Reported listings and reviews'}\n help={pluginDocsHelp('abuseReports', {\n excerpt:\n 'Reports users filed against a marketplace listing or a ' +\n 'review, with the reason each one gave. Closing one needs ' +\n 'a note, and every change is audited.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <TextField\n select\n size=\"small\"\n value={statusFilter}\n onChange={(event) => {\n setReports(null)\n setStatusFilter(event.target.value)\n }}\n >\n <MenuItem value=\"open\">{'Open'}</MenuItem>\n <MenuItem value=\"reviewing\">{'Reviewing'}</MenuItem>\n <MenuItem value=\"actioned\">{'Actioned'}</MenuItem>\n <MenuItem value=\"dismissed\">{'Dismissed'}</MenuItem>\n <MenuItem value=\"\">{'Everything'}</MenuItem>\n </TextField>\n ),\n }}\n >\n {more ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {'These are the most recently updated reports in this ' +\n 'state, and there are older ones not shown. Moving a ' +\n 'report on takes it out of this list.'}\n </Alert>\n ) : null}\n {!identityVisible ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {'Reporter accounts are hidden at your access level. Every ' +\n 'report still shows whether there is somebody behind it.'}\n </Alert>\n ) : null}\n {reports === null ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {loading ? 'Reading reports…' : ''}\n </Typography>\n ) : reports.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Nothing in this state.'}\n </Typography>\n ) : (\n <Stack spacing={3}>\n {reports.map((report) => (\n <Stack key={report.id} spacing={1}>\n <Stack\n direction=\"row\"\n sx={{ flexWrap: 'wrap', gap: 1, alignItems: 'center' }}\n >\n <Chip\n size=\"small\"\n color={STATUS_COLOR[report.status] ?? 'default'}\n label={report.status}\n />\n <Chip size=\"small\" variant=\"outlined\" label={report.targetType} />\n <Typography variant=\"subtitle2\">\n {report.listingName || report.listingId || report.id}\n </Typography>\n </Stack>\n {/* THE REASON, in full. This is the whole content of a\n report; a queue that summarised it would be the\n original silence with an extra click in it. */}\n <Typography variant=\"body2\">\n {report.reason || '(no reason given)'}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n report.publisherOrgId\n ? `publisher ${report.publisherOrgId}`\n : null,\n // A REDACTED reporter and an ANONYMOUS one are\n // different facts: only the second means there is\n // nobody to follow up with.\n report.reporterUid\n ? `reporter ${report.reporterUid}`\n : report.reporterKnown\n ? 'reporter hidden'\n : 'no reporter recorded',\n `filed ${when(report.createdAtMs)}`,\n report.resolvedByEmail\n ? `closed by ${report.resolvedByEmail}`\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n {report.resolution ? (\n <Alert severity=\"success\">{report.resolution}</Alert>\n ) : null}\n <Stack\n direction=\"row\"\n sx={{ flexWrap: 'wrap', gap: 1, alignItems: 'center' }}\n >\n <TextField\n size=\"small\"\n placeholder=\"What did you do, and why?\"\n value={notes[report.id] ?? ''}\n onChange={(event) =>\n setNotes((previous) => ({\n ...previous,\n [report.id]: event.target.value,\n }))\n }\n sx={{ flex: 1, minWidth: 260 }}\n />\n <Button\n size=\"small\"\n disabled={busyId === report.id}\n onClick={() => void transition(report, 'reviewing')}\n >\n {'Reviewing'}\n </Button>\n <Button\n size=\"small\"\n color=\"success\"\n disabled={busyId === report.id}\n onClick={() => void transition(report, 'actioned')}\n >\n {'Actioned'}\n </Button>\n <Button\n size=\"small\"\n color=\"warning\"\n disabled={busyId === report.id}\n onClick={() => void transition(report, 'dismissed')}\n >\n {'Dismiss'}\n </Button>\n </Stack>\n </Stack>\n ))}\n </Stack>\n )}\n </CardDisplay>\n </Stack>\n </>\n )\n}\nMarketplaceReportsPage.displayName = 'MarketplaceReportsPage'\n\nexport default MarketplaceReportsPage\n"],"names":["PageHeaderHelp","pluginDocsHelp","CardDisplay","useSnackbar","Alert","Button","Chip","LinearProgress","MenuItem","Stack","TextField","Typography","useCallback","useEffect","useState","useUser","authorizedFetch","STATUS_COLOR","open","reviewing","actioned","dismissed","when","ms","Date","toLocaleString","MarketplaceReportsPage","data","user","enqueueSnackbar","reports","setReports","identityVisible","setIdentityVisible","more","setMore","statusFilter","setStatusFilter","error","setError","loading","setLoading","busyId","setBusyId","notes","setNotes","epoch","setEpoch","active","response","encodeURIComponent","body","json","catch","ok","Boolean","transition","report","status","id","method","headers","JSON","stringify","resolution","payload","variant","allowDuplicate","persist","value","topic","anchor","spacing","severity","header","help","excerpt","contentGutterX","contentGutterY","HeaderProps","action","select","size","onChange","event","target","sx","mb","color","length","map","direction","flexWrap","gap","alignItems","label","targetType","listingName","listingId","reason","publisherOrgId","reporterUid","reporterKnown","createdAtMs","resolvedByEmail","filter","join","placeholder","previous","flex","minWidth","disabled","onClick","displayName"],"mappings":"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"}