@aglyn/plugins-marketplace 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +51 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
- package/src/lib/components/artifact-update-dialog.component.js +244 -0
- package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
- package/src/lib/components/host-plugins-card.component.d.ts +23 -0
- package/src/lib/components/host-plugins-card.component.js +566 -0
- package/src/lib/components/host-plugins-card.component.js.map +1 -0
- package/src/lib/components/listing-content.component.d.ts +57 -0
- package/src/lib/components/listing-content.component.js +1880 -0
- package/src/lib/components/listing-content.component.js.map +1 -0
- package/src/lib/components/listing-image.component.d.ts +46 -0
- package/src/lib/components/listing-image.component.js +38 -0
- package/src/lib/components/listing-image.component.js.map +1 -0
- package/src/lib/components/listing-reviews.component.d.ts +22 -0
- package/src/lib/components/listing-reviews.component.js +419 -0
- package/src/lib/components/listing-reviews.component.js.map +1 -0
- package/src/lib/components/marketplace-browse.component.d.ts +38 -0
- package/src/lib/components/marketplace-browse.component.js +802 -0
- package/src/lib/components/marketplace-browse.component.js.map +1 -0
- package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
- package/src/lib/components/plugin-site-set-panel.component.js +137 -0
- package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
- package/src/lib/components/plugin-site-set.component.d.ts +43 -0
- package/src/lib/components/plugin-site-set.component.js +425 -0
- package/src/lib/components/plugin-site-set.component.js.map +1 -0
- package/src/lib/components/rating-input.component.d.ts +3 -0
- package/src/lib/components/rating-input.component.js +47 -0
- package/src/lib/components/rating-input.component.js.map +1 -0
- package/src/lib/components/report-target.component.d.ts +36 -0
- package/src/lib/components/report-target.component.js +181 -0
- package/src/lib/components/report-target.component.js.map +1 -0
- package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
- package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
- package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-artifact-update.d.ts +41 -0
- package/src/lib/hooks/use-artifact-update.js +169 -0
- package/src/lib/hooks/use-artifact-update.js.map +1 -0
- package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
- package/src/lib/hooks/use-marketplace-actions.js +523 -0
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
- package/src/lib/model/artifact-merge.d.ts +154 -0
- package/src/lib/model/artifact-merge.js +228 -0
- package/src/lib/model/artifact-merge.js.map +1 -0
- package/src/lib/model/index.d.ts +43 -0
- package/src/lib/model/index.js +41 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketplace-props.d.ts +57 -0
- package/src/lib/model/marketplace-props.js +441 -0
- package/src/lib/model/marketplace-props.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +833 -0
- package/src/lib/model/marketplace.js +929 -0
- package/src/lib/model/marketplace.js.map +1 -0
- package/src/lib/model/rating-field.d.ts +24 -0
- package/src/lib/model/rating-field.js +33 -0
- package/src/lib/model/rating-field.js.map +1 -0
- package/src/lib/plugin.d.ts +33 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +25 -0
- package/src/lib/server/billing-webhook.js +1062 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/checkout.d.ts +76 -0
- package/src/lib/server/checkout.js +477 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +24 -0
- package/src/lib/server/connect.js +205 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/install-dataset-schema.d.ts +32 -0
- package/src/lib/server/install-dataset-schema.js +255 -0
- package/src/lib/server/install-dataset-schema.js.map +1 -0
- package/src/lib/server/install-email-starter.d.ts +65 -0
- package/src/lib/server/install-email-starter.js +279 -0
- package/src/lib/server/install-email-starter.js.map +1 -0
- package/src/lib/server/install-email-template.d.ts +34 -0
- package/src/lib/server/install-email-template.js +252 -0
- package/src/lib/server/install-email-template.js.map +1 -0
- package/src/lib/server/install-layout.d.ts +29 -0
- package/src/lib/server/install-layout.js +323 -0
- package/src/lib/server/install-layout.js.map +1 -0
- package/src/lib/server/install-pin-counts.d.ts +173 -0
- package/src/lib/server/install-pin-counts.js +248 -0
- package/src/lib/server/install-pin-counts.js.map +1 -0
- package/src/lib/server/install-plugin.d.ts +18 -0
- package/src/lib/server/install-plugin.js +405 -0
- package/src/lib/server/install-plugin.js.map +1 -0
- package/src/lib/server/install-template.d.ts +37 -0
- package/src/lib/server/install-template.js +348 -0
- package/src/lib/server/install-template.js.map +1 -0
- package/src/lib/server/install-theme.d.ts +42 -0
- package/src/lib/server/install-theme.js +313 -0
- package/src/lib/server/install-theme.js.map +1 -0
- package/src/lib/server/install.d.ts +27 -0
- package/src/lib/server/install.js +276 -0
- package/src/lib/server/install.js.map +1 -0
- package/src/lib/server/listing-versions.d.ts +26 -0
- package/src/lib/server/listing-versions.js +378 -0
- package/src/lib/server/listing-versions.js.map +1 -0
- package/src/lib/server/preview-image.d.ts +37 -0
- package/src/lib/server/preview-image.js +216 -0
- package/src/lib/server/preview-image.js.map +1 -0
- package/src/lib/server/provenance.d.ts +100 -0
- package/src/lib/server/provenance.js +118 -0
- package/src/lib/server/provenance.js.map +1 -0
- package/src/lib/server/publish-dataset-schema.d.ts +31 -0
- package/src/lib/server/publish-dataset-schema.js +202 -0
- package/src/lib/server/publish-dataset-schema.js.map +1 -0
- package/src/lib/server/publish-email-starter.d.ts +44 -0
- package/src/lib/server/publish-email-starter.js +231 -0
- package/src/lib/server/publish-email-starter.js.map +1 -0
- package/src/lib/server/publish-email-template.d.ts +33 -0
- package/src/lib/server/publish-email-template.js +211 -0
- package/src/lib/server/publish-email-template.js.map +1 -0
- package/src/lib/server/publish-layout.d.ts +33 -0
- package/src/lib/server/publish-layout.js +243 -0
- package/src/lib/server/publish-layout.js.map +1 -0
- package/src/lib/server/publish-plugin.d.ts +18 -0
- package/src/lib/server/publish-plugin.js +485 -0
- package/src/lib/server/publish-plugin.js.map +1 -0
- package/src/lib/server/publish-preconditions.d.ts +86 -0
- package/src/lib/server/publish-preconditions.js +160 -0
- package/src/lib/server/publish-preconditions.js.map +1 -0
- package/src/lib/server/publish-template.d.ts +27 -0
- package/src/lib/server/publish-template.js +215 -0
- package/src/lib/server/publish-template.js.map +1 -0
- package/src/lib/server/publish-theme.d.ts +38 -0
- package/src/lib/server/publish-theme.js +186 -0
- package/src/lib/server/publish-theme.js.map +1 -0
- package/src/lib/server/publish.d.ts +26 -0
- package/src/lib/server/publish.js +204 -0
- package/src/lib/server/publish.js.map +1 -0
- package/src/lib/server/publisher-profile-save.d.ts +30 -0
- package/src/lib/server/publisher-profile-save.js +226 -0
- package/src/lib/server/publisher-profile-save.js.map +1 -0
- package/src/lib/server/publisher-profile.d.ts +86 -0
- package/src/lib/server/publisher-profile.js +116 -0
- package/src/lib/server/publisher-profile.js.map +1 -0
- package/src/lib/server/purchase-entitlement.d.ts +83 -0
- package/src/lib/server/purchase-entitlement.js +118 -0
- package/src/lib/server/purchase-entitlement.js.map +1 -0
- package/src/lib/server/report.d.ts +32 -0
- package/src/lib/server/report.js +101 -0
- package/src/lib/server/report.js.map +1 -0
- package/src/lib/server/reviews.d.ts +41 -0
- package/src/lib/server/reviews.js +240 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/update-artifact.d.ts +19 -0
- package/src/lib/server/update-artifact.js +569 -0
- package/src/lib/server/update-artifact.js.map +1 -0
- package/src/lib/server/verification-request.d.ts +31 -0
- package/src/lib/server/verification-request.js +173 -0
- package/src/lib/server/verification-request.js.map +1 -0
- package/src/lib/server/version-stats.d.ts +202 -0
- package/src/lib/server/version-stats.js +305 -0
- package/src/lib/server/version-stats.js.map +1 -0
- package/src/lib/server.d.ts +30 -0
- package/src/lib/server.js +95 -0
- package/src/lib/server.js.map +1 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import { mdiCheckDecagram } from "@aglyn/shared-data-mdi";
|
|
20
|
+
import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
22
|
+
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
23
|
+
import { Alert, Chip, Tooltip, Grid, MenuItem, Rating, Stack, TextField, Typography } from "@mui/material";
|
|
24
|
+
import { collection, doc, documentId, getDoc, limit, orderBy, query, where } from "firebase/firestore";
|
|
25
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
26
|
+
import { buildRoute, PLATFORM_BRAND_NAME, pluginDocsHelp, Route } from "@aglyn/aglyn";
|
|
27
|
+
import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useFirestoreDoc, useHostOrgId, useUser, useScopeTokens } from "@aglyn/tenant-feature-instance";
|
|
28
|
+
/*
|
|
29
|
+
* The MODULE, not the barrel, for the two PURE helpers — the specs that render
|
|
30
|
+
* this grid mock `@aglyn/tenant-feature-instance` wholesale to stage their
|
|
31
|
+
* Firestore hooks, and a query builder imported through that barrel disappears
|
|
32
|
+
* under the mock. Neither of these is a hook.
|
|
33
|
+
*/ import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
|
|
34
|
+
import { isListingBrowsable, isListingDeleted, isPrivateListing, listingArtifactType, listingArtifactLabel, resolvePluginInstallState } from "../model/marketplace.js";
|
|
35
|
+
import { ListingImage } from "./listing-image.component.js";
|
|
36
|
+
// The console route table is shared (AGL-685), so these go through
|
|
37
|
+
// buildRoute rather than being reassembled from a base string — the shape
|
|
38
|
+
// of `/[orgSlug]/hosts/[host]/marketplace/…` is not this plugin's to know.
|
|
39
|
+
/**
|
|
40
|
+
* How many install records the shelf reads to decide what it already runs.
|
|
41
|
+
*
|
|
42
|
+
* One number for the five collections that answer that question, because the
|
|
43
|
+
* card reads them together and a reader comparing a plugin's chip to a
|
|
44
|
+
* dataset's should not have to know which window was twice the other. It
|
|
45
|
+
* bounds an install-state LOOKUP, not the shelf: the listings query has its
|
|
46
|
+
* own cap, and the grid pages what survives the filters.
|
|
47
|
+
*/ const INSTALL_STATE_CEILING = 100;
|
|
48
|
+
/**
|
|
49
|
+
* Marketplace components browse (AGL-44). A read-only catalogue: each card links
|
|
50
|
+
* to the listing's detail page, which is the only place an install happens
|
|
51
|
+
* (AGL-867) — installing from a grid card was too easy and skipped the
|
|
52
|
+
* site-targeting choice. The card still shows install STATE (installed,
|
|
53
|
+
* org-wide) so the shelf reads honestly, but carries no install action.
|
|
54
|
+
*/ export function MarketplaceBrowse(props) {
|
|
55
|
+
const { hostId } = props;
|
|
56
|
+
const firestore = useFirestore();
|
|
57
|
+
const { data: user } = useUser();
|
|
58
|
+
const { orgScoped, orgSlug: orgSlugProp, publisherId } = props;
|
|
59
|
+
const [handles, setHandles] = useState({});
|
|
60
|
+
// Listings are org-owned (AGL-652), so "is this mine" is an org comparison.
|
|
61
|
+
// Resolved from the routing mirror rather than a new prop so the component
|
|
62
|
+
// stays self-contained; hostIndex is signed-in readable.
|
|
63
|
+
const [viewerOrgId, setViewerOrgId] = useState(null);
|
|
64
|
+
useEffect(()=>{
|
|
65
|
+
let active = true;
|
|
66
|
+
void getDoc(doc(firestore, 'hostIndex', hostId)).then((snapshot)=>{
|
|
67
|
+
var _snapshot_get;
|
|
68
|
+
if (active) setViewerOrgId((_snapshot_get = snapshot.get('orgId')) != null ? _snapshot_get : null);
|
|
69
|
+
}).catch(()=>undefined);
|
|
70
|
+
return ()=>{
|
|
71
|
+
active = false;
|
|
72
|
+
};
|
|
73
|
+
}, [
|
|
74
|
+
firestore,
|
|
75
|
+
hostId
|
|
76
|
+
]);
|
|
77
|
+
// Card links were built as `/{hostDocId}/marketplace/…`, a shape that has
|
|
78
|
+
// not resolved since AGL-621/622 — every listing and publisher link on
|
|
79
|
+
// this grid 404'd. One shared resolution (AGL-673); null renders plain
|
|
80
|
+
// text rather than a link to nowhere.
|
|
81
|
+
const { orgSlug: resolvedOrgSlug, subdomain } = useConsoleHostRoute(hostId);
|
|
82
|
+
// Prefer the URL-supplied slug (AGL-867) — synchronous and always present at
|
|
83
|
+
// org scope — over the async host resolution, which the per-site route still
|
|
84
|
+
// relies on.
|
|
85
|
+
const orgSlug = orgSlugProp != null ? orgSlugProp : resolvedOrgSlug;
|
|
86
|
+
// Link targets differ by surface (AGL-772): the org marketplace resolves
|
|
87
|
+
// to the org route (`/[orgSlug]/marketplace/[listingId]`), the per-site tab
|
|
88
|
+
// to the host route. Null → plain text, never a link to nowhere. Publisher
|
|
89
|
+
// pages have no org route yet, so they stay text at org scope for now.
|
|
90
|
+
const listingHref = (listingId)=>orgScoped ? orgSlug ? buildRoute(Route.ORG_MARKETPLACE_LISTING, {
|
|
91
|
+
orgSlug,
|
|
92
|
+
listingId
|
|
93
|
+
}) : undefined : orgSlug && subdomain ? buildRoute(Route.ORG_MARKETPLACE_LISTING, {
|
|
94
|
+
orgSlug,
|
|
95
|
+
listingId
|
|
96
|
+
}) : undefined;
|
|
97
|
+
// Storefront links carry the publisher's HANDLE (AGL-1001), the identity
|
|
98
|
+
// shown right beside them on the card. The id remains a valid segment on
|
|
99
|
+
// the page itself, so a publisher whose handle hasn't loaded yet still
|
|
100
|
+
// links somewhere real rather than nowhere.
|
|
101
|
+
const publisherHref = (profileId)=>{
|
|
102
|
+
var _handles_profileId, _handles_profileId1;
|
|
103
|
+
return orgScoped ? // listings. Needs the URL slug, which is passed in at org scope.
|
|
104
|
+
orgSlug ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {
|
|
105
|
+
orgSlug,
|
|
106
|
+
handle: (_handles_profileId = handles[profileId]) != null ? _handles_profileId : profileId
|
|
107
|
+
}) : undefined : orgSlug && subdomain ? buildRoute(Route.ORG_MARKETPLACE_PUBLISHER, {
|
|
108
|
+
orgSlug,
|
|
109
|
+
handle: (_handles_profileId1 = handles[profileId]) != null ? _handles_profileId1 : profileId
|
|
110
|
+
}) : undefined;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* NO `where('deletedAt','==',null)` HERE — soft-deleted listings are dropped
|
|
114
|
+
* in `items` below instead (AGL-1196).
|
|
115
|
+
*
|
|
116
|
+
* `deletedAt` flips every time a publisher unpublishes or republishes. A
|
|
117
|
+
* document that stops matching a live query leaves the query target, and the
|
|
118
|
+
* client can cache a `noDocument` tombstone at its own path — which is then
|
|
119
|
+
* served to every reader of that path, including the detail page, which
|
|
120
|
+
* reads `marketplaceListings/{id}` BY ID. So a browse session open across an
|
|
121
|
+
* unpublish could 404 a listing that exists, and republishing would not
|
|
122
|
+
* clear it: a resumed listen only pulls deltas, so an otherwise-unchanged
|
|
123
|
+
* document is never re-sent (AGL-827, AGL-929).
|
|
124
|
+
*
|
|
125
|
+
* Dropping the predicate removes the mechanism rather than healing it: with
|
|
126
|
+
* no `where`, no document can stop matching. The rule permits it —
|
|
127
|
+
* `marketplaceListings` is `allow read: if true`, with no `resource.data`
|
|
128
|
+
* term, so an unconstrained list is not denied. (That is NOT true of the
|
|
129
|
+
* scoped `datasets` query below, which is why this fix does not generalise.)
|
|
130
|
+
*
|
|
131
|
+
* It also fixes a quieter bug: `== null` matches only an EXPLICIT null, and
|
|
132
|
+
* Firestore cannot express "field is absent". Every publish path stamps
|
|
133
|
+
* `deletedAt: null` today, so this happens to be exact — but any future
|
|
134
|
+
* write path that omits the field would make a live listing silently
|
|
135
|
+
* invisible. An in-memory falsy check treats absent as live, which is what
|
|
136
|
+
* the dataset and email-template filters in this file already do.
|
|
137
|
+
*
|
|
138
|
+
* The cap counts CANDIDATES, not results — private and unreviewed listings
|
|
139
|
+
* already consume slots and get filtered below, so this is the same class of
|
|
140
|
+
* consumption, nudged up to leave room for soft-deleted rows. Measured on
|
|
141
|
+
* production 2026-08-03: 7 listings, 0 soft-deleted, 0 missing the field.
|
|
142
|
+
*
|
|
143
|
+
* ## The cap DOES take an `orderBy`, and it is not the same decision
|
|
144
|
+
*
|
|
145
|
+
* A `limit` with no ordering is answered in document-id order, and listing
|
|
146
|
+
* ids are random — so the ninety this loaded were an arbitrary ninety, and
|
|
147
|
+
* the client sort below then arranged that sample by date and called it
|
|
148
|
+
* "Newest". The window is now the newest ninety, which is what the default
|
|
149
|
+
* sort already claimed it was.
|
|
150
|
+
*
|
|
151
|
+
* `orderBy` carries the drop the `where` above was removed to avoid — it
|
|
152
|
+
* matches only documents that HAVE the field, so a listing without
|
|
153
|
+
* `createdAt` would not be mis-ordered, it would be invisible. That is safe
|
|
154
|
+
* here for a reason the `deletedAt` predicate could not offer: a listing is
|
|
155
|
+
* created by exactly two paths, `publish.ts` and `publish-plugin.ts`, and
|
|
156
|
+
* both stamp `createdAt` in the same `set` that brings the document into
|
|
157
|
+
* existence. Every other writer resolves an EXISTING listing by id and
|
|
158
|
+
* refuses when it is absent, so none of them can produce one without the
|
|
159
|
+
* field.
|
|
160
|
+
*
|
|
161
|
+
* It also cannot reproduce the tombstone (AGL-827/929) that made a mutable
|
|
162
|
+
* `where` unsafe: `createdAt` is written under `existing.empty` and never
|
|
163
|
+
* rewritten, so no document can stop matching mid-session and leave a
|
|
164
|
+
* `noDocument` cached at a path the detail page reads by id.
|
|
165
|
+
*/ const { data: listings } = useFirestoreCollection(()=>query(collection(firestore, 'marketplaceListings'), orderBy('createdAt', 'desc'), limit(90)), [
|
|
166
|
+
firestore
|
|
167
|
+
], {
|
|
168
|
+
idField: '$id'
|
|
169
|
+
});
|
|
170
|
+
/*
|
|
171
|
+
* ## The install-state lookups are ORDERED too, and they are not lists
|
|
172
|
+
*
|
|
173
|
+
* The five queries below (this one, the two pin collections, the org
|
|
174
|
+
* datasets and the site's email templates) answer one question per card:
|
|
175
|
+
* has this listing already been installed here. None of them is rendered —
|
|
176
|
+
* each is folded into a map keyed by listing id — so the fix that suits the
|
|
177
|
+
* shelf above does not suit them: there is no page to walk and no footer to
|
|
178
|
+
* put a control in.
|
|
179
|
+
*
|
|
180
|
+
* What they share with it is the defect. A bare `limit` is answered in
|
|
181
|
+
* DOCUMENT-ID order, so an unnamed window is an arbitrary slice of its
|
|
182
|
+
* collection, and a lookup MISS is indistinguishable from a genuine
|
|
183
|
+
* not-installed: a workspace past such a window is shown "Add to this site"
|
|
184
|
+
* beside something it already runs, and the detail page — which reads its
|
|
185
|
+
* own pins by id — disagrees with the grid it was reached from.
|
|
186
|
+
*
|
|
187
|
+
* `collectionCeiling` names the order and asks for one row past the
|
|
188
|
+
* ceiling, which is what turns "the window may have bitten" into a fact the
|
|
189
|
+
* shelf can state rather than a silence it cannot.
|
|
190
|
+
*/ const { data: installedRead } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'components'), INSTALL_STATE_CEILING), [
|
|
191
|
+
firestore,
|
|
192
|
+
hostId
|
|
193
|
+
], {
|
|
194
|
+
idField: '$id'
|
|
195
|
+
});
|
|
196
|
+
const { rows: installedDocs, truncated: installedTruncated } = useMemo(()=>ceilingedWindow(installedRead, INSTALL_STATE_CEILING), [
|
|
197
|
+
installedRead
|
|
198
|
+
]);
|
|
199
|
+
// listingId → installed component doc (deleted installs don't count).
|
|
200
|
+
const installed = useMemo(()=>{
|
|
201
|
+
const map = {};
|
|
202
|
+
for (const definition of installedDocs != null ? installedDocs : []){
|
|
203
|
+
var _definition_marketplace;
|
|
204
|
+
const listingId = definition == null ? void 0 : (_definition_marketplace = definition.marketplace) == null ? void 0 : _definition_marketplace.listingId;
|
|
205
|
+
if (listingId && !definition.deletedAt) map[listingId] = definition;
|
|
206
|
+
}
|
|
207
|
+
return map;
|
|
208
|
+
}, [
|
|
209
|
+
installedDocs
|
|
210
|
+
]);
|
|
211
|
+
// Plugin installs are version PINS, not component snapshots (AGL-656): the
|
|
212
|
+
// `components` map above never holds one, so a plugin already installed —
|
|
213
|
+
// at host or org scope — showed "Add to this site". These two pin
|
|
214
|
+
// collections are what the loader honors (a host pin shadows an org pin).
|
|
215
|
+
const orgId = useHostOrgId(hostId);
|
|
216
|
+
const { data: hostPinRead } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'installs'), INSTALL_STATE_CEILING), [
|
|
217
|
+
firestore,
|
|
218
|
+
hostId
|
|
219
|
+
], {
|
|
220
|
+
idField: '$id'
|
|
221
|
+
});
|
|
222
|
+
const { rows: hostPinDocs, truncated: hostPinsTruncated } = useMemo(()=>ceilingedWindow(hostPinRead, INSTALL_STATE_CEILING), [
|
|
223
|
+
hostPinRead
|
|
224
|
+
]);
|
|
225
|
+
// Held at null while `useHostOrgId` is in flight, never `orgs/-pending-`
|
|
226
|
+
// (AGL-1440): the AGL-1047 comment seven lines below records the same
|
|
227
|
+
// denial-every-mount shape for `useScopeTokens` — this listen had it too.
|
|
228
|
+
const { data: orgPinRead } = useFirestoreCollection(()=>orgId ? collectionCeiling(collection(firestore, 'orgs', orgId, 'installs'), INSTALL_STATE_CEILING) : null, [
|
|
229
|
+
firestore,
|
|
230
|
+
orgId
|
|
231
|
+
], {
|
|
232
|
+
idField: '$id'
|
|
233
|
+
});
|
|
234
|
+
const { rows: orgPinDocs, truncated: orgPinsTruncated } = useMemo(()=>ceilingedWindow(orgPinRead, INSTALL_STATE_CEILING), [
|
|
235
|
+
orgPinRead
|
|
236
|
+
]);
|
|
237
|
+
// Scoped (AGL-1044): an unfiltered list is REJECTED for a scoped member,
|
|
238
|
+
// not filtered, so this would error without the constraint.
|
|
239
|
+
//
|
|
240
|
+
// Wait for the member doc before listing (AGL-1047). `useScopeTokens`
|
|
241
|
+
// reports `orgWide: true` while loading, so without `scopeLoaded` a scoped
|
|
242
|
+
// collaborator's first render computes `needsScope: false` and sends an
|
|
243
|
+
// UNFILTERED list that the AGL-1041 rules deny per document. It recovers
|
|
244
|
+
// on the next render, which is what makes it easy to miss: the page looks
|
|
245
|
+
// right and logs a denial every mount.
|
|
246
|
+
const { tokens: scopeTokens, orgWide: viewerOrgWide, loaded: scopeLoaded } = useScopeTokens(orgId != null ? orgId : undefined);
|
|
247
|
+
const needsScope = Boolean(orgId) && !viewerOrgWide;
|
|
248
|
+
// The AGL-657 types land in neither the components collection nor a pin
|
|
249
|
+
// (AGL-789): a dataset schema becomes an org dataset, an email template a
|
|
250
|
+
// draft version. Both installers stamp the source listing, so read those.
|
|
251
|
+
/*
|
|
252
|
+
* `documentId()` rather than a field, for the reason the audience sweep in
|
|
253
|
+
* `campaign-send.ts` gives: Firestore's automatic single-field index for an
|
|
254
|
+
* array member is keyed on the value and the document name, so
|
|
255
|
+
* `array-contains-any` plus `orderBy(__name__)` is served by it. Ordering on
|
|
256
|
+
* anything else would need a composite index per scope shape.
|
|
257
|
+
*
|
|
258
|
+
* The ceiling is the shared one. It is not sized for the dataset
|
|
259
|
+
* collection: it is sized for the number of LISTINGS whose install state a
|
|
260
|
+
* shelf of ninety cards can describe, which is the same question its four
|
|
261
|
+
* neighbours answer, so it is the same number.
|
|
262
|
+
*/ const { data: datasetRead } = useFirestoreCollection(()=>orgId && scopeLoaded ? query(collection(firestore, 'orgs', orgId, 'datasets'), ...needsScope ? [
|
|
263
|
+
where('visibleTo', 'array-contains-any', scopeTokens)
|
|
264
|
+
] : [], orderBy(documentId()), limit(INSTALL_STATE_CEILING + 1)) : null, [
|
|
265
|
+
firestore,
|
|
266
|
+
orgId,
|
|
267
|
+
scopeLoaded,
|
|
268
|
+
needsScope,
|
|
269
|
+
scopeTokens
|
|
270
|
+
], // `visibleTo` is MUTABLE — every scope edit rewrites it — so this query
|
|
271
|
+
// can tombstone a dataset the way AGL-827 tombstoned a host. The rule
|
|
272
|
+
// requires the constraint for anyone who is not org-wide, so unlike the
|
|
273
|
+
// listings query above the predicate cannot simply be dropped (AGL-1196).
|
|
274
|
+
{
|
|
275
|
+
idField: '$id',
|
|
276
|
+
confirmDisappearances: true
|
|
277
|
+
});
|
|
278
|
+
const { rows: datasetDocs, truncated: datasetsTruncated } = useMemo(()=>ceilingedWindow(datasetRead, INSTALL_STATE_CEILING), [
|
|
279
|
+
datasetRead
|
|
280
|
+
]);
|
|
281
|
+
const { data: emailRead } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'emailTemplates'), INSTALL_STATE_CEILING), [
|
|
282
|
+
firestore,
|
|
283
|
+
hostId
|
|
284
|
+
], {
|
|
285
|
+
idField: '$id'
|
|
286
|
+
});
|
|
287
|
+
const { rows: emailDocs, truncated: emailsTruncated } = useMemo(()=>ceilingedWindow(emailRead, INSTALL_STATE_CEILING), [
|
|
288
|
+
emailRead
|
|
289
|
+
]);
|
|
290
|
+
// A theme is not a document in a collection — it is a field on the site
|
|
291
|
+
// (AGL-1020) — so its install is read from the host doc rather than from a
|
|
292
|
+
// per-artifact query like the two above.
|
|
293
|
+
const { data: hostDoc } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId), [
|
|
294
|
+
firestore,
|
|
295
|
+
hostId
|
|
296
|
+
], {
|
|
297
|
+
idField: '$id'
|
|
298
|
+
});
|
|
299
|
+
// listingId → the newest install of it. A schema install deliberately makes
|
|
300
|
+
// a NEW dataset every time, so this is genuinely one-to-many; the highest
|
|
301
|
+
// installed version is what the card should speak for.
|
|
302
|
+
const artifactInstalls = useMemo(()=>{
|
|
303
|
+
var _hostDoc_themeInstalledFrom, _hostDoc_themeInstalledFrom1;
|
|
304
|
+
const map = {};
|
|
305
|
+
const note = (listingId, version)=>{
|
|
306
|
+
var _map_id;
|
|
307
|
+
const id = listingId ? String(listingId) : '';
|
|
308
|
+
if (!id) return;
|
|
309
|
+
const next = version != null ? String(version) : null;
|
|
310
|
+
const seen = (_map_id = map[id]) == null ? void 0 : _map_id.version;
|
|
311
|
+
if (!seen || next && next > seen) map[id] = {
|
|
312
|
+
version: next
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
for (const dataset of datasetDocs != null ? datasetDocs : []){
|
|
316
|
+
var _dataset_source, _dataset_source1;
|
|
317
|
+
if (dataset.deletedAt) continue;
|
|
318
|
+
note((_dataset_source = dataset.source) == null ? void 0 : _dataset_source.listingId, (_dataset_source1 = dataset.source) == null ? void 0 : _dataset_source1.version);
|
|
319
|
+
}
|
|
320
|
+
for (const template of emailDocs != null ? emailDocs : []){
|
|
321
|
+
var _template_installedFrom, _template_installedFrom1;
|
|
322
|
+
if (template.deletedAt) continue;
|
|
323
|
+
note((_template_installedFrom = template.installedFrom) == null ? void 0 : _template_installedFrom.listingId, (_template_installedFrom1 = template.installedFrom) == null ? void 0 : _template_installedFrom1.version);
|
|
324
|
+
}
|
|
325
|
+
note(hostDoc == null ? void 0 : (_hostDoc_themeInstalledFrom = hostDoc.themeInstalledFrom) == null ? void 0 : _hostDoc_themeInstalledFrom.listingId, hostDoc == null ? void 0 : (_hostDoc_themeInstalledFrom1 = hostDoc.themeInstalledFrom) == null ? void 0 : _hostDoc_themeInstalledFrom1.version);
|
|
326
|
+
return map;
|
|
327
|
+
}, [
|
|
328
|
+
datasetDocs,
|
|
329
|
+
emailDocs,
|
|
330
|
+
hostDoc
|
|
331
|
+
]);
|
|
332
|
+
const hostPins = useMemo(()=>{
|
|
333
|
+
const map = {};
|
|
334
|
+
for (const pin of hostPinDocs != null ? hostPinDocs : [])map[pin.$id] = pin;
|
|
335
|
+
return map;
|
|
336
|
+
}, [
|
|
337
|
+
hostPinDocs
|
|
338
|
+
]);
|
|
339
|
+
const orgPins = useMemo(()=>{
|
|
340
|
+
const map = {};
|
|
341
|
+
for (const pin of orgPinDocs != null ? orgPinDocs : [])map[pin.$id] = pin;
|
|
342
|
+
return map;
|
|
343
|
+
}, [
|
|
344
|
+
orgPinDocs
|
|
345
|
+
]);
|
|
346
|
+
// Resolve each listing's publisher handle once. `handles` must NOT be a
|
|
347
|
+
// dependency here: the effect writes `handles`, so listing it would make the
|
|
348
|
+
// effect re-run on its own output. During the post-load window, when
|
|
349
|
+
// `listings` arrives while every other subscription on this always-mounted
|
|
350
|
+
// grid is also settling, that self-retrigger adds render+effect cycles to an
|
|
351
|
+
// already dense flurry — enough that a concurrent update elsewhere can trip
|
|
352
|
+
// React's nested-update limit (AGL-785). A ref of already-requested ids
|
|
353
|
+
// dedupes instead, so the effect depends only on `listings`.
|
|
354
|
+
const requestedHandles = useRef(new Set());
|
|
355
|
+
useEffect(()=>{
|
|
356
|
+
const profileIds = [
|
|
357
|
+
...new Set((listings != null ? listings : []).map((listing)=>listing.profileId))
|
|
358
|
+
].filter((profileId)=>profileId && !requestedHandles.current.has(String(profileId)));
|
|
359
|
+
if (!profileIds.length) return;
|
|
360
|
+
for (const profileId of profileIds)requestedHandles.current.add(String(profileId));
|
|
361
|
+
let cancelled = false;
|
|
362
|
+
Promise.all(profileIds.map(async (profileId)=>{
|
|
363
|
+
var _ref;
|
|
364
|
+
const snapshot = await getDoc(doc(firestore, 'publisherProfiles', String(profileId))).catch(()=>null);
|
|
365
|
+
return [
|
|
366
|
+
profileId,
|
|
367
|
+
(_ref = snapshot == null ? void 0 : snapshot.get('handle')) != null ? _ref : ''
|
|
368
|
+
];
|
|
369
|
+
})).then((entries)=>{
|
|
370
|
+
if (!cancelled) {
|
|
371
|
+
setHandles((prev)=>_extends({}, prev, Object.fromEntries(entries)));
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return ()=>{
|
|
375
|
+
cancelled = true;
|
|
376
|
+
};
|
|
377
|
+
}, [
|
|
378
|
+
listings,
|
|
379
|
+
firestore
|
|
380
|
+
]);
|
|
381
|
+
// Browse controls (AGL-95): client-side search/filter/sort over the
|
|
382
|
+
// fetched page of listings.
|
|
383
|
+
const [search, setSearch] = useState('');
|
|
384
|
+
const [category, setCategory] = useState(null);
|
|
385
|
+
const [sort, setSort] = useState('newest');
|
|
386
|
+
const categories = useMemo(()=>[
|
|
387
|
+
...new Set((listings != null ? listings : []).map((listing)=>listing.category).filter(Boolean))
|
|
388
|
+
].sort(), [
|
|
389
|
+
listings
|
|
390
|
+
]);
|
|
391
|
+
const items = useMemo(()=>{
|
|
392
|
+
const needle = search.trim().toLowerCase();
|
|
393
|
+
const filtered = (listings != null ? listings : []).filter((listing)=>{
|
|
394
|
+
// Private plugins never appear here, not even for the org that owns
|
|
395
|
+
// them (AGL-968/993). The owner exemption below exists so a publisher
|
|
396
|
+
// can watch their own SUBMISSION move through review — but a private
|
|
397
|
+
// listing is not waiting to be listed, it is deliberately not for the
|
|
398
|
+
// marketplace, and showing it in this grid is the one thing "private"
|
|
399
|
+
// promises will not happen. Owners reach theirs from
|
|
400
|
+
// Marketplace › Listings, whose View opens the same detail page
|
|
401
|
+
// installs happen on.
|
|
402
|
+
// Soft-deleted listings used to be excluded by the query itself; they
|
|
403
|
+
// are dropped here now so no mutable field sits in a `where`
|
|
404
|
+
// (AGL-1196). Unconditional — unlike the review gate below, deletion
|
|
405
|
+
// has no owner exemption.
|
|
406
|
+
if (isListingDeleted(listing)) return false;
|
|
407
|
+
if (isPrivateListing(listing)) return false;
|
|
408
|
+
// Review queue gate (AGL-432): unreviewed/rejected plugin listings
|
|
409
|
+
// stay off the public browse; the owner still sees their own (the
|
|
410
|
+
// detail page shows them the status). UX-level only — the docs are
|
|
411
|
+
// public-readable by design.
|
|
412
|
+
if (!isListingBrowsable(listing) && listing.profileId !== viewerOrgId) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
// Publisher page (AGL-869): only this publisher's listings.
|
|
416
|
+
if (publisherId && listing.profileId !== publisherId) return false;
|
|
417
|
+
if (category && listing.category !== category) return false;
|
|
418
|
+
if (!needle) return true;
|
|
419
|
+
return [
|
|
420
|
+
listing.displayName,
|
|
421
|
+
listing.description,
|
|
422
|
+
listing.category
|
|
423
|
+
].filter(Boolean).some((value)=>value.toLowerCase().includes(needle));
|
|
424
|
+
});
|
|
425
|
+
return [
|
|
426
|
+
...filtered
|
|
427
|
+
].sort((a, b)=>{
|
|
428
|
+
var _ref, _ref1;
|
|
429
|
+
var _b_createdAt, _a_createdAt;
|
|
430
|
+
if (sort === 'installed') {
|
|
431
|
+
var _b_installCount, _a_installCount;
|
|
432
|
+
return ((_b_installCount = b.installCount) != null ? _b_installCount : 0) - ((_a_installCount = a.installCount) != null ? _a_installCount : 0);
|
|
433
|
+
}
|
|
434
|
+
if (sort === 'rated') {
|
|
435
|
+
var _b_ratingAverage, _a_ratingAverage, _b_ratingCount, _a_ratingCount;
|
|
436
|
+
// Unrated listings sort last rather than as zero-stars — a new
|
|
437
|
+
// listing has not been judged badly, it has not been judged.
|
|
438
|
+
const byAverage = ((_b_ratingAverage = b.ratingAverage) != null ? _b_ratingAverage : -1) - ((_a_ratingAverage = a.ratingAverage) != null ? _a_ratingAverage : -1);
|
|
439
|
+
return byAverage || ((_b_ratingCount = b.ratingCount) != null ? _b_ratingCount : 0) - ((_a_ratingCount = a.ratingCount) != null ? _a_ratingCount : 0);
|
|
440
|
+
}
|
|
441
|
+
return ((_ref = (_b_createdAt = b.createdAt) == null ? void 0 : _b_createdAt.seconds) != null ? _ref : 0) - ((_ref1 = (_a_createdAt = a.createdAt) == null ? void 0 : _a_createdAt.seconds) != null ? _ref1 : 0);
|
|
442
|
+
});
|
|
443
|
+
}, [
|
|
444
|
+
listings,
|
|
445
|
+
search,
|
|
446
|
+
category,
|
|
447
|
+
sort,
|
|
448
|
+
publisherId,
|
|
449
|
+
user == null ? void 0 : user.uid
|
|
450
|
+
]);
|
|
451
|
+
/*
|
|
452
|
+
* The shelf PAGES (AGL-2501). It used to render every card the window held
|
|
453
|
+
* in one wall, so the only control over how much arrived at once was the
|
|
454
|
+
* cap in the query — a number the reader cannot see and cannot change.
|
|
455
|
+
*
|
|
456
|
+
* Client-side, deliberately, and this is the case `ListPagination`'s
|
|
457
|
+
* `count` prop exists for. The search box, the category chips and the
|
|
458
|
+
* owner/review gates are all resolved in memory, so a server page would be
|
|
459
|
+
* a page of CANDIDATES: a reader filtering to one category would get pages
|
|
460
|
+
* that were mostly empty and a count that meant nothing. Paging what is
|
|
461
|
+
* left after the filter is the only version whose numbers are about what is
|
|
462
|
+
* on screen.
|
|
463
|
+
*/ /**
|
|
464
|
+
* The install-state lookups did not read everything they were asked about.
|
|
465
|
+
*
|
|
466
|
+
* Said on the SHELF rather than on the card that is wrong, because there is
|
|
467
|
+
* no way to know which card that is: a miss and a genuine not-installed are
|
|
468
|
+
* the same absence from the same map. What the reader can act on is that the
|
|
469
|
+
* chips are a floor — anything marked installed is installed, and something
|
|
470
|
+
* unmarked may still be.
|
|
471
|
+
*/ const installStateTruncated = installedTruncated || hostPinsTruncated || orgPinsTruncated || datasetsTruncated || emailsTruncated;
|
|
472
|
+
const [page, setPage] = useState(0);
|
|
473
|
+
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
474
|
+
/*
|
|
475
|
+
* A new query starts at page one. Page four of a category with two pages
|
|
476
|
+
* does not exist, and an out-of-range page renders as an empty grid with
|
|
477
|
+
* nothing saying why — which reads as the filter having broken.
|
|
478
|
+
*/ useEffect(()=>{
|
|
479
|
+
setPage(0);
|
|
480
|
+
}, [
|
|
481
|
+
search,
|
|
482
|
+
category,
|
|
483
|
+
sort,
|
|
484
|
+
pageSize
|
|
485
|
+
]);
|
|
486
|
+
const shown = useMemo(()=>items.slice(page * pageSize, (page + 1) * pageSize), [
|
|
487
|
+
items,
|
|
488
|
+
page,
|
|
489
|
+
pageSize
|
|
490
|
+
]);
|
|
491
|
+
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
492
|
+
header: 'Marketplace components',
|
|
493
|
+
help: pluginDocsHelp('plugins', {
|
|
494
|
+
anchor: '#install--upgrade'
|
|
495
|
+
}),
|
|
496
|
+
contentGutterX: true,
|
|
497
|
+
contentGutterY: true,
|
|
498
|
+
children: [
|
|
499
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
500
|
+
direction: "row",
|
|
501
|
+
spacing: 1,
|
|
502
|
+
sx: {
|
|
503
|
+
mb: 2,
|
|
504
|
+
alignItems: 'center',
|
|
505
|
+
flexWrap: 'wrap',
|
|
506
|
+
rowGap: 1
|
|
507
|
+
},
|
|
508
|
+
children: [
|
|
509
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
510
|
+
placeholder: "Search components…",
|
|
511
|
+
value: search,
|
|
512
|
+
onChange: (event)=>setSearch(event.target.value),
|
|
513
|
+
size: "small",
|
|
514
|
+
sx: {
|
|
515
|
+
minWidth: 200
|
|
516
|
+
}
|
|
517
|
+
}),
|
|
518
|
+
categories.map((value)=>/*#__PURE__*/ _jsx(Chip, {
|
|
519
|
+
label: value,
|
|
520
|
+
variant: category === value ? 'filled' : 'outlined',
|
|
521
|
+
color: category === value ? 'primary' : 'default',
|
|
522
|
+
onClick: ()=>setCategory((previous)=>previous === value ? null : value)
|
|
523
|
+
}, value)),
|
|
524
|
+
/*#__PURE__*/ _jsxs(TextField, {
|
|
525
|
+
value: sort,
|
|
526
|
+
onChange: (event)=>setSort(event.target.value),
|
|
527
|
+
size: "small",
|
|
528
|
+
select: true,
|
|
529
|
+
sx: {
|
|
530
|
+
ml: 'auto',
|
|
531
|
+
minWidth: 150
|
|
532
|
+
},
|
|
533
|
+
children: [
|
|
534
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
535
|
+
value: "newest",
|
|
536
|
+
children: 'Newest'
|
|
537
|
+
}),
|
|
538
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
539
|
+
value: "installed",
|
|
540
|
+
children: 'Most installed'
|
|
541
|
+
}),
|
|
542
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
543
|
+
value: "rated",
|
|
544
|
+
children: 'Highest rated'
|
|
545
|
+
})
|
|
546
|
+
]
|
|
547
|
+
})
|
|
548
|
+
]
|
|
549
|
+
}),
|
|
550
|
+
installStateTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
551
|
+
severity: "info",
|
|
552
|
+
sx: {
|
|
553
|
+
mb: 2
|
|
554
|
+
},
|
|
555
|
+
children: `Installed state is resolved from the first ${INSTALL_STATE_CEILING} ` + 'install records in this workspace, ordered by id. There are ' + 'more, so a card may not be marked installed even though it is. ' + 'Open a listing to see what this workspace actually runs.'
|
|
556
|
+
}) : null,
|
|
557
|
+
items.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
558
|
+
variant: "body2",
|
|
559
|
+
color: "text.secondary",
|
|
560
|
+
children: 'No marketplace components published yet — publish one of your ' + 'reusable components from the Setup page to be the first.'
|
|
561
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
562
|
+
children: [
|
|
563
|
+
/*#__PURE__*/ _jsx(Grid, {
|
|
564
|
+
container: true,
|
|
565
|
+
spacing: 2,
|
|
566
|
+
children: shown.map((listing)=>{
|
|
567
|
+
var _ref, _listing_priceUsd, _listing_ratingAverage, _listing_ratingAverage1;
|
|
568
|
+
var _componentInstall_marketplace;
|
|
569
|
+
const artifactType = listingArtifactType(listing);
|
|
570
|
+
const isPlugin = artifactType === 'plugin';
|
|
571
|
+
const pluginState = resolvePluginInstallState(listing.latestVersion, isPlugin ? hostPins[listing.$id] : null, isPlugin ? orgPins[listing.$id] : null);
|
|
572
|
+
const componentInstall = installed[listing.$id];
|
|
573
|
+
// datasetSchema/emailTemplate installs are tracked by the source
|
|
574
|
+
// listing stamped on what they created (AGL-789).
|
|
575
|
+
//
|
|
576
|
+
// `emailStarter` is deliberately absent. Every other type has ONE
|
|
577
|
+
// install per site to point at — a pin, a dataset, a draft on a
|
|
578
|
+
// fixed catalog key, the site's theme — so "Installed" names
|
|
579
|
+
// something. A starter installs as a NEW email each time, and a
|
|
580
|
+
// site may keep five of them; there is no single copy for a chip
|
|
581
|
+
// to describe, and marking the listing installed would grey out
|
|
582
|
+
// the button that makes the sixth.
|
|
583
|
+
const artifactInstall = artifactType === 'datasetSchema' || artifactType === 'emailTemplate' || artifactType === 'theme' ? artifactInstalls[listing.$id] : undefined;
|
|
584
|
+
const isInstalled = isPlugin ? pluginState.scope != null : Boolean(componentInstall != null ? componentInstall : artifactInstall);
|
|
585
|
+
const installedVersion = isPlugin ? pluginState.installedVersion : (_ref = componentInstall == null ? void 0 : (_componentInstall_marketplace = componentInstall.marketplace) == null ? void 0 : _componentInstall_marketplace.version) != null ? _ref : artifactInstall == null ? void 0 : artifactInstall.version;
|
|
586
|
+
const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
|
|
587
|
+
const detailHref = listingHref(listing.$id);
|
|
588
|
+
return /*#__PURE__*/ _jsx(Grid, {
|
|
589
|
+
size: {
|
|
590
|
+
xs: 12,
|
|
591
|
+
sm: 6,
|
|
592
|
+
md: 4
|
|
593
|
+
},
|
|
594
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
595
|
+
spacing: 1,
|
|
596
|
+
sx: {
|
|
597
|
+
border: 1,
|
|
598
|
+
borderColor: 'divider',
|
|
599
|
+
borderRadius: 1,
|
|
600
|
+
p: 2,
|
|
601
|
+
height: '100%'
|
|
602
|
+
},
|
|
603
|
+
children: [
|
|
604
|
+
/*#__PURE__*/ _jsx(ListingImage, {
|
|
605
|
+
src: listing.previewImageUrl,
|
|
606
|
+
alt: `${listing.displayName} preview`,
|
|
607
|
+
sx: {
|
|
608
|
+
width: '100%',
|
|
609
|
+
height: 120,
|
|
610
|
+
objectFit: 'cover',
|
|
611
|
+
borderRadius: 1
|
|
612
|
+
}
|
|
613
|
+
}),
|
|
614
|
+
/*#__PURE__*/ _jsx(AppLink, {
|
|
615
|
+
href: listingHref(listing.$id),
|
|
616
|
+
color: "inherit",
|
|
617
|
+
underline: "hover",
|
|
618
|
+
variant: "subtitle2",
|
|
619
|
+
children: listing.displayName
|
|
620
|
+
}),
|
|
621
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
622
|
+
direction: "row",
|
|
623
|
+
spacing: 1,
|
|
624
|
+
sx: {
|
|
625
|
+
alignItems: 'center',
|
|
626
|
+
flexWrap: 'wrap',
|
|
627
|
+
rowGap: 1
|
|
628
|
+
},
|
|
629
|
+
children: [
|
|
630
|
+
/*#__PURE__*/ _jsx(Chip, {
|
|
631
|
+
size: "small",
|
|
632
|
+
color: "primary",
|
|
633
|
+
label: listingArtifactLabel(listing)
|
|
634
|
+
}),
|
|
635
|
+
listing.category ? /*#__PURE__*/ _jsx(Chip, {
|
|
636
|
+
size: "small",
|
|
637
|
+
label: listing.category
|
|
638
|
+
}) : null,
|
|
639
|
+
listing.reviewStatus === 'verified' ? /*#__PURE__*/ _jsx(Tooltip, {
|
|
640
|
+
title: `A human at ${PLATFORM_BRAND_NAME} confirmed who ` + 'this publisher is, ' + 'and that the listing describes what the code does. ' + 'It is a claim about the publisher, not about this ' + 'release — it survives a version bump.',
|
|
641
|
+
children: /*#__PURE__*/ _jsx(Chip, {
|
|
642
|
+
size: "small",
|
|
643
|
+
color: "info",
|
|
644
|
+
label: "Verified publisher",
|
|
645
|
+
// Carries more weight than the neighbouring
|
|
646
|
+
// classification chips on purpose: it is the only one
|
|
647
|
+
// that says a human vouched for the code, and the
|
|
648
|
+
// rest are just taxonomy. The icon is what makes it
|
|
649
|
+
// findable when scanning a grid rather than reading
|
|
650
|
+
// one card.
|
|
651
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
652
|
+
path: mdiCheckDecagram.path,
|
|
653
|
+
sx: {
|
|
654
|
+
fontSize: 16
|
|
655
|
+
}
|
|
656
|
+
}),
|
|
657
|
+
sx: {
|
|
658
|
+
fontWeight: 600
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
}) : null,
|
|
662
|
+
listing.latestVersionReviewState === 'approved' ? /*#__PURE__*/ _jsx(Tooltip, {
|
|
663
|
+
title: `A human at ${PLATFORM_BRAND_NAME} read these exact ` + 'bytes — the ' + 'version on offer — against a required checklist. ' + 'Re-earned per version, so a new release starts ' + 'without it. Not a security guarantee: every plugin ' + 'runs in the same sandbox either way.',
|
|
664
|
+
children: /*#__PURE__*/ _jsx(Chip, {
|
|
665
|
+
size: "small",
|
|
666
|
+
color: "success",
|
|
667
|
+
label: "Reviewed"
|
|
668
|
+
})
|
|
669
|
+
}) : null,
|
|
670
|
+
/*#__PURE__*/ _jsx(Chip, {
|
|
671
|
+
size: "small",
|
|
672
|
+
color: priceUsd > 0 ? 'primary' : 'success',
|
|
673
|
+
variant: priceUsd > 0 ? 'filled' : 'outlined',
|
|
674
|
+
label: priceUsd > 0 ? `$${priceUsd}` : 'Free'
|
|
675
|
+
})
|
|
676
|
+
]
|
|
677
|
+
}),
|
|
678
|
+
/*#__PURE__*/ _jsxs(Typography, {
|
|
679
|
+
variant: "caption",
|
|
680
|
+
color: "text.secondary",
|
|
681
|
+
children: [
|
|
682
|
+
`v${listing.latestVersion}`,
|
|
683
|
+
handles[listing.profileId] ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
684
|
+
children: [
|
|
685
|
+
' · by ',
|
|
686
|
+
/*#__PURE__*/ _jsx(AppLink, {
|
|
687
|
+
href: publisherHref(listing.profileId),
|
|
688
|
+
color: "primary",
|
|
689
|
+
underline: "hover",
|
|
690
|
+
children: `@${handles[listing.profileId]}`
|
|
691
|
+
})
|
|
692
|
+
]
|
|
693
|
+
}) : '',
|
|
694
|
+
listing.installCount ? ` · ${listing.installCount} install${listing.installCount === 1 ? '' : 's'}` : ''
|
|
695
|
+
]
|
|
696
|
+
}),
|
|
697
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
698
|
+
direction: "row",
|
|
699
|
+
spacing: 0.5,
|
|
700
|
+
sx: {
|
|
701
|
+
alignItems: 'center'
|
|
702
|
+
},
|
|
703
|
+
children: listing.ratingCount ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
704
|
+
children: [
|
|
705
|
+
/*#__PURE__*/ _jsx(Rating, {
|
|
706
|
+
size: "small",
|
|
707
|
+
readOnly: true,
|
|
708
|
+
precision: 0.1,
|
|
709
|
+
value: Number((_listing_ratingAverage = listing.ratingAverage) != null ? _listing_ratingAverage : 0)
|
|
710
|
+
}),
|
|
711
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
712
|
+
variant: "caption",
|
|
713
|
+
color: "text.secondary",
|
|
714
|
+
children: `${(_listing_ratingAverage1 = listing.ratingAverage) != null ? _listing_ratingAverage1 : 0} (${listing.ratingCount})`
|
|
715
|
+
})
|
|
716
|
+
]
|
|
717
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
718
|
+
variant: "caption",
|
|
719
|
+
color: "text.secondary",
|
|
720
|
+
children: 'Not yet rated'
|
|
721
|
+
})
|
|
722
|
+
}),
|
|
723
|
+
listing.description ? /*#__PURE__*/ _jsx(Typography, {
|
|
724
|
+
variant: "body2",
|
|
725
|
+
color: "text.secondary",
|
|
726
|
+
sx: {
|
|
727
|
+
flex: 1
|
|
728
|
+
},
|
|
729
|
+
// Four lines, then ellipsis (AGL-1002). Grid rows
|
|
730
|
+
// stretch to their tallest card, so one publisher's
|
|
731
|
+
// essay used to leave every card beside it mostly white
|
|
732
|
+
// space; the full text is one click away.
|
|
733
|
+
//
|
|
734
|
+
// Plain `style`, not `sx`: emotion drops the
|
|
735
|
+
// `-webkit-box` display value on the way through, and
|
|
736
|
+
// without it the other three properties clamp nothing
|
|
737
|
+
// (measured — `display` computed to `flow-root` while
|
|
738
|
+
// line-clamp and box-orient came through fine). Nothing
|
|
739
|
+
// here reads the theme, so there is nothing to lose.
|
|
740
|
+
style: {
|
|
741
|
+
display: '-webkit-box',
|
|
742
|
+
WebkitBoxOrient: 'vertical',
|
|
743
|
+
WebkitLineClamp: 4,
|
|
744
|
+
overflow: 'hidden'
|
|
745
|
+
},
|
|
746
|
+
children: listing.description
|
|
747
|
+
}) : /*#__PURE__*/ _jsx("span", {
|
|
748
|
+
style: {
|
|
749
|
+
flex: 1
|
|
750
|
+
}
|
|
751
|
+
}),
|
|
752
|
+
isInstalled ? /*#__PURE__*/ _jsxs(Stack, {
|
|
753
|
+
direction: "row",
|
|
754
|
+
spacing: 0.75,
|
|
755
|
+
sx: {
|
|
756
|
+
alignItems: 'center',
|
|
757
|
+
flexWrap: 'wrap'
|
|
758
|
+
},
|
|
759
|
+
children: [
|
|
760
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
761
|
+
variant: "caption",
|
|
762
|
+
color: "success.main",
|
|
763
|
+
children: `Installed${installedVersion ? ` (v${installedVersion})` : ''}`
|
|
764
|
+
}),
|
|
765
|
+
isPlugin && pluginState.scope === 'org' ? /*#__PURE__*/ _jsx(Chip, {
|
|
766
|
+
size: "small",
|
|
767
|
+
variant: "outlined",
|
|
768
|
+
label: pluginState.shadowed ? 'Org-wide (shadowed)' : 'Org-wide'
|
|
769
|
+
}) : null
|
|
770
|
+
]
|
|
771
|
+
}) : null,
|
|
772
|
+
/*#__PURE__*/ _jsx(AppLink, {
|
|
773
|
+
componentVariant: "button",
|
|
774
|
+
size: "small",
|
|
775
|
+
variant: "outlined",
|
|
776
|
+
color: "primary",
|
|
777
|
+
href: detailHref != null ? detailHref : '',
|
|
778
|
+
disabled: !detailHref,
|
|
779
|
+
children: priceUsd > 0 && !isInstalled ? `View details · $${priceUsd}` : 'View details'
|
|
780
|
+
})
|
|
781
|
+
]
|
|
782
|
+
})
|
|
783
|
+
}, listing.$id);
|
|
784
|
+
})
|
|
785
|
+
}),
|
|
786
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
787
|
+
page: page,
|
|
788
|
+
pageSize: pageSize,
|
|
789
|
+
rowCount: shown.length,
|
|
790
|
+
count: items.length,
|
|
791
|
+
onPageChange: setPage,
|
|
792
|
+
onPageSizeChange: setPageSize
|
|
793
|
+
})
|
|
794
|
+
]
|
|
795
|
+
})
|
|
796
|
+
]
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
MarketplaceBrowse.displayName = 'MarketplaceBrowse';
|
|
800
|
+
export default MarketplaceBrowse;
|
|
801
|
+
|
|
802
|
+
//# sourceMappingURL=marketplace-browse.component.js.map
|