@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,419 @@
|
|
|
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 { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
19
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
20
|
+
import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollection, useUser } from "@aglyn/tenant-feature-instance";
|
|
21
|
+
import { Alert, Avatar, Button, Chip, Divider, Rating, Stack, TextField, Typography } from "@mui/material";
|
|
22
|
+
import { collection } from "firebase/firestore";
|
|
23
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
24
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
25
|
+
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
26
|
+
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
27
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
28
|
+
import ReportTarget from "./report-target.component.js";
|
|
29
|
+
/**
|
|
30
|
+
* How many review documents this card reads.
|
|
31
|
+
*
|
|
32
|
+
* A CEILING, not a page size. Three things below are computed over the whole
|
|
33
|
+
* set — the hidden-review filter, the newest-first sort, and finding the
|
|
34
|
+
* reader's OWN review to seed the form — so a server page would be a page of
|
|
35
|
+
* candidates and a reader whose review sat past the first ten would be shown
|
|
36
|
+
* an empty form over a review they had already written.
|
|
37
|
+
*/ const REVIEW_CEILING = 200;
|
|
38
|
+
/**
|
|
39
|
+
* Ratings and comments on a listing (AGL-655).
|
|
40
|
+
*
|
|
41
|
+
* Reads the subcollection directly — it is public-read, so there is no
|
|
42
|
+
* value in proxying it through an API. Writes go through
|
|
43
|
+
* `/api/marketplace/reviews`, which owns every rule that matters: only
|
|
44
|
+
* accounts that installed the listing may rate it, the publishing org
|
|
45
|
+
* cannot review itself, and the aggregates it maintains are frozen from
|
|
46
|
+
* client writes.
|
|
47
|
+
*
|
|
48
|
+
* The star input is offered to everyone and refused server-side rather than
|
|
49
|
+
* hidden, because "you must install this to rate it" is worth saying out
|
|
50
|
+
* loud — a control that silently does nothing teaches nothing.
|
|
51
|
+
*/ export function ListingReviews({ listingId, listing }) {
|
|
52
|
+
var _ref, _ref1;
|
|
53
|
+
const firestore = useFirestore();
|
|
54
|
+
const { data: user } = useUser();
|
|
55
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
56
|
+
const uid = user == null ? void 0 : user.uid;
|
|
57
|
+
// Rating (not commenting) requires a verified email (AGL-865). We can't see
|
|
58
|
+
// the install pins from here, so the "installed" half is still enforced
|
|
59
|
+
// server-side; the email half we can reflect in the control's state.
|
|
60
|
+
const emailVerified = Boolean(user == null ? void 0 : user.emailVerified);
|
|
61
|
+
const { data: reviewDocs } = useFirestoreCollection(/*
|
|
62
|
+
* ORDERED AND CEILINGED, with a probe (AGL-2501).
|
|
63
|
+
*
|
|
64
|
+
* `limit(100)` alone is answered in DOCUMENT-ID order, and a review's
|
|
65
|
+
* document id is its author's uid — so the hundred were a pseudo-random
|
|
66
|
+
* hundred of the reviewers, and on a popular listing the reviews past
|
|
67
|
+
* them were not merely unrendered but unreachable, because nothing drew
|
|
68
|
+
* them and no control asked for more.
|
|
69
|
+
*
|
|
70
|
+
* Ordering on the document NAME changes which rows come back only in the
|
|
71
|
+
* sense that the walk is now total: every reviewer is reachable by
|
|
72
|
+
* paging. It is not chronological and nothing here claims it is — the
|
|
73
|
+
* sort below puts the newest first, which it may do because it holds the
|
|
74
|
+
* whole ceiling rather than a slice of it.
|
|
75
|
+
*
|
|
76
|
+
* `orderBy('updatedAtMs')` would have been the tempting fix and is the
|
|
77
|
+
* dangerous one: it matches only documents that HAVE the field, so a
|
|
78
|
+
* review written before that field existed would vanish from a listing's
|
|
79
|
+
* page rather than sort oddly on it.
|
|
80
|
+
*/ ()=>collectionCeiling(collection(firestore, 'marketplaceListings', listingId, 'reviews'), REVIEW_CEILING), [
|
|
81
|
+
firestore,
|
|
82
|
+
listingId
|
|
83
|
+
], {
|
|
84
|
+
idField: '$id'
|
|
85
|
+
});
|
|
86
|
+
const { rows: readReviews, truncated: reviewsTruncated } = ceilingedWindow(reviewDocs, REVIEW_CEILING);
|
|
87
|
+
const reviews = useMemo(()=>[
|
|
88
|
+
...readReviews
|
|
89
|
+
].filter((entry)=>!entry.hidden).sort((a, b)=>{
|
|
90
|
+
var _b_updatedAtMs, _a_updatedAtMs;
|
|
91
|
+
return ((_b_updatedAtMs = b.updatedAtMs) != null ? _b_updatedAtMs : 0) - ((_a_updatedAtMs = a.updatedAtMs) != null ? _a_updatedAtMs : 0);
|
|
92
|
+
}), // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
[
|
|
94
|
+
reviewDocs
|
|
95
|
+
]);
|
|
96
|
+
const mine = useMemo(()=>reviews.find((entry)=>entry.$id === uid), [
|
|
97
|
+
reviews,
|
|
98
|
+
uid
|
|
99
|
+
]);
|
|
100
|
+
/*
|
|
101
|
+
* The page is a SLICE of what the card already holds, for the three reasons
|
|
102
|
+
* the ceiling exists: `hidden` is filtered after reading, the order on
|
|
103
|
+
* screen is not the order the query walks, and `mine` seeds the form above
|
|
104
|
+
* from anywhere in the set.
|
|
105
|
+
*/ const [page, setPage] = useState(0);
|
|
106
|
+
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
107
|
+
const visibleReviews = useMemo(()=>reviews.slice(page * pageSize, page * pageSize + pageSize), [
|
|
108
|
+
reviews,
|
|
109
|
+
page,
|
|
110
|
+
pageSize
|
|
111
|
+
]);
|
|
112
|
+
/*
|
|
113
|
+
* A new listing starts at page one. Page four of a listing with two pages
|
|
114
|
+
* does not exist, and an out-of-range page renders as an empty list with
|
|
115
|
+
* nothing saying why — which reads as the reviews having gone.
|
|
116
|
+
*/ useEffect(()=>{
|
|
117
|
+
setPage(0);
|
|
118
|
+
}, [
|
|
119
|
+
listingId
|
|
120
|
+
]);
|
|
121
|
+
const [rating, setRating] = useState(null);
|
|
122
|
+
const [comment, setComment] = useState('');
|
|
123
|
+
const [busy, setBusy] = useState(false);
|
|
124
|
+
useEffect(()=>{
|
|
125
|
+
var _ref, _ref1;
|
|
126
|
+
setRating((_ref = mine == null ? void 0 : mine.rating) != null ? _ref : null);
|
|
127
|
+
setComment((_ref1 = mine == null ? void 0 : mine.comment) != null ? _ref1 : '');
|
|
128
|
+
}, [
|
|
129
|
+
mine
|
|
130
|
+
]);
|
|
131
|
+
const submit = useCallback(async ()=>{
|
|
132
|
+
if (busy || !rating && !comment.trim()) return;
|
|
133
|
+
setBusy(true);
|
|
134
|
+
try {
|
|
135
|
+
const response = await authorizedFetch(user, '/api/marketplace/reviews', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json'
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
listingId,
|
|
142
|
+
rating,
|
|
143
|
+
comment: comment.trim()
|
|
144
|
+
})
|
|
145
|
+
});
|
|
146
|
+
const payload = await response.json().catch(()=>({}));
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
var _ref;
|
|
149
|
+
// 403 here is usually "you have not installed this" — actionable,
|
|
150
|
+
// so it reads as guidance rather than an error.
|
|
151
|
+
return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Could not post that', {
|
|
152
|
+
variant: response.status === 403 ? 'warning' : 'error',
|
|
153
|
+
allowDuplicate: true
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
enqueueSnackbar(mine ? 'Updated your review' : 'Thanks for the review', {
|
|
157
|
+
variant: 'success',
|
|
158
|
+
persist: false
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error(error);
|
|
162
|
+
enqueueSnackbar('Could not post that', {
|
|
163
|
+
variant: 'error',
|
|
164
|
+
allowDuplicate: true
|
|
165
|
+
});
|
|
166
|
+
} finally{
|
|
167
|
+
setBusy(false);
|
|
168
|
+
}
|
|
169
|
+
}, [
|
|
170
|
+
busy,
|
|
171
|
+
rating,
|
|
172
|
+
comment,
|
|
173
|
+
user,
|
|
174
|
+
listingId,
|
|
175
|
+
mine,
|
|
176
|
+
enqueueSnackbar
|
|
177
|
+
]);
|
|
178
|
+
const remove = useCallback(async ()=>{
|
|
179
|
+
if (busy || !mine) return;
|
|
180
|
+
setBusy(true);
|
|
181
|
+
try {
|
|
182
|
+
await authorizedFetch(user, '/api/marketplace/reviews', {
|
|
183
|
+
method: 'DELETE',
|
|
184
|
+
headers: {
|
|
185
|
+
'Content-Type': 'application/json'
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
listingId
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
setRating(null);
|
|
192
|
+
setComment('');
|
|
193
|
+
enqueueSnackbar('Removed your review', {
|
|
194
|
+
variant: 'success',
|
|
195
|
+
persist: false
|
|
196
|
+
});
|
|
197
|
+
} finally{
|
|
198
|
+
setBusy(false);
|
|
199
|
+
}
|
|
200
|
+
}, [
|
|
201
|
+
busy,
|
|
202
|
+
mine,
|
|
203
|
+
user,
|
|
204
|
+
listingId,
|
|
205
|
+
enqueueSnackbar
|
|
206
|
+
]);
|
|
207
|
+
const average = Number((_ref = listing == null ? void 0 : listing.ratingAverage) != null ? _ref : 0);
|
|
208
|
+
const ratingCount = Number((_ref1 = listing == null ? void 0 : listing.ratingCount) != null ? _ref1 : 0);
|
|
209
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
210
|
+
header: 'Ratings & comments',
|
|
211
|
+
help: pluginDocsHelp('installYourFirstPlugin', {
|
|
212
|
+
anchor: '#step-3-reviews'
|
|
213
|
+
}),
|
|
214
|
+
contentGutterX: true,
|
|
215
|
+
contentGutterY: true,
|
|
216
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
217
|
+
spacing: 2,
|
|
218
|
+
children: [
|
|
219
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
220
|
+
direction: "row",
|
|
221
|
+
spacing: 1,
|
|
222
|
+
sx: {
|
|
223
|
+
alignItems: 'center'
|
|
224
|
+
},
|
|
225
|
+
children: ratingCount ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
226
|
+
children: [
|
|
227
|
+
/*#__PURE__*/ _jsx(Rating, {
|
|
228
|
+
value: average,
|
|
229
|
+
precision: 0.1,
|
|
230
|
+
size: "small",
|
|
231
|
+
readOnly: true
|
|
232
|
+
}),
|
|
233
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
234
|
+
variant: "body2",
|
|
235
|
+
color: "text.secondary",
|
|
236
|
+
children: `${average} · ${ratingCount} rating${ratingCount === 1 ? '' : 's'}`
|
|
237
|
+
})
|
|
238
|
+
]
|
|
239
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
240
|
+
variant: "body2",
|
|
241
|
+
color: "text.secondary",
|
|
242
|
+
children: 'No ratings yet.'
|
|
243
|
+
})
|
|
244
|
+
}),
|
|
245
|
+
uid ? /*#__PURE__*/ _jsxs(Stack, {
|
|
246
|
+
spacing: 1,
|
|
247
|
+
children: [
|
|
248
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
249
|
+
variant: "subtitle2",
|
|
250
|
+
children: mine ? 'Your review' : 'Leave a review'
|
|
251
|
+
}),
|
|
252
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
253
|
+
direction: "row",
|
|
254
|
+
spacing: 1,
|
|
255
|
+
sx: {
|
|
256
|
+
alignItems: 'center'
|
|
257
|
+
},
|
|
258
|
+
children: [
|
|
259
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
260
|
+
variant: "caption",
|
|
261
|
+
sx: {
|
|
262
|
+
minWidth: 56
|
|
263
|
+
},
|
|
264
|
+
children: 'Rating'
|
|
265
|
+
}),
|
|
266
|
+
/*#__PURE__*/ _jsx(Rating, {
|
|
267
|
+
value: rating,
|
|
268
|
+
size: "small",
|
|
269
|
+
onChange: (_event, value)=>setRating(value),
|
|
270
|
+
disabled: busy || !emailVerified
|
|
271
|
+
}),
|
|
272
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
273
|
+
variant: "caption",
|
|
274
|
+
color: "text.secondary",
|
|
275
|
+
children: emailVerified ? 'Requires having installed this' : 'Verify your email to rate'
|
|
276
|
+
})
|
|
277
|
+
]
|
|
278
|
+
}),
|
|
279
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
280
|
+
size: "small",
|
|
281
|
+
label: "Comment",
|
|
282
|
+
placeholder: "Share how it worked out, or ask a question — anyone can comment",
|
|
283
|
+
value: comment,
|
|
284
|
+
onChange: (event)=>setComment(event.target.value),
|
|
285
|
+
disabled: busy,
|
|
286
|
+
multiline: true,
|
|
287
|
+
minRows: 2,
|
|
288
|
+
fullWidth: true
|
|
289
|
+
}),
|
|
290
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
291
|
+
direction: "row",
|
|
292
|
+
spacing: 1,
|
|
293
|
+
children: [
|
|
294
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
295
|
+
size: "small",
|
|
296
|
+
variant: "contained",
|
|
297
|
+
color: "primary",
|
|
298
|
+
disabled: busy || !rating && !comment.trim(),
|
|
299
|
+
onClick: ()=>void submit(),
|
|
300
|
+
children: busy ? 'Posting…' : mine ? 'Update' : 'Post'
|
|
301
|
+
}),
|
|
302
|
+
mine ? /*#__PURE__*/ _jsx(Button, {
|
|
303
|
+
size: "small",
|
|
304
|
+
disabled: busy,
|
|
305
|
+
onClick: ()=>void remove(),
|
|
306
|
+
children: 'Remove'
|
|
307
|
+
}) : null
|
|
308
|
+
]
|
|
309
|
+
})
|
|
310
|
+
]
|
|
311
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
312
|
+
variant: "body2",
|
|
313
|
+
color: "text.secondary",
|
|
314
|
+
children: 'Sign in to leave a rating or comment.'
|
|
315
|
+
}),
|
|
316
|
+
reviews.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
317
|
+
children: [
|
|
318
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
319
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
320
|
+
spacing: 2,
|
|
321
|
+
children: visibleReviews.map((review)=>{
|
|
322
|
+
var _review_displayName;
|
|
323
|
+
const name = String((_review_displayName = review.displayName) != null ? _review_displayName : 'Someone');
|
|
324
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
325
|
+
direction: "row",
|
|
326
|
+
spacing: 1.5,
|
|
327
|
+
sx: {
|
|
328
|
+
alignItems: 'flex-start'
|
|
329
|
+
},
|
|
330
|
+
children: [
|
|
331
|
+
/*#__PURE__*/ _jsx(Avatar, {
|
|
332
|
+
sx: {
|
|
333
|
+
width: 32,
|
|
334
|
+
height: 32,
|
|
335
|
+
fontSize: 14
|
|
336
|
+
},
|
|
337
|
+
children: name.trim().charAt(0).toUpperCase() || '?'
|
|
338
|
+
}),
|
|
339
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
340
|
+
spacing: 0.25,
|
|
341
|
+
sx: {
|
|
342
|
+
flex: 1,
|
|
343
|
+
minWidth: 0
|
|
344
|
+
},
|
|
345
|
+
children: [
|
|
346
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
347
|
+
direction: "row",
|
|
348
|
+
spacing: 1,
|
|
349
|
+
sx: {
|
|
350
|
+
alignItems: 'center',
|
|
351
|
+
flexWrap: 'wrap'
|
|
352
|
+
},
|
|
353
|
+
children: [
|
|
354
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
355
|
+
variant: "body2",
|
|
356
|
+
sx: {
|
|
357
|
+
fontWeight: 600
|
|
358
|
+
},
|
|
359
|
+
children: name
|
|
360
|
+
}),
|
|
361
|
+
review.verifiedInstaller ? /*#__PURE__*/ _jsx(Chip, {
|
|
362
|
+
size: "small",
|
|
363
|
+
label: "Installed",
|
|
364
|
+
color: "primary"
|
|
365
|
+
}) : null,
|
|
366
|
+
review.updatedAtMs ? /*#__PURE__*/ _jsx(Typography, {
|
|
367
|
+
variant: "caption",
|
|
368
|
+
color: "text.secondary",
|
|
369
|
+
children: new Date(review.updatedAtMs).toLocaleDateString()
|
|
370
|
+
}) : null
|
|
371
|
+
]
|
|
372
|
+
}),
|
|
373
|
+
review.rating ? /*#__PURE__*/ _jsx(Rating, {
|
|
374
|
+
value: review.rating,
|
|
375
|
+
size: "small",
|
|
376
|
+
readOnly: true
|
|
377
|
+
}) : null,
|
|
378
|
+
review.comment ? /*#__PURE__*/ _jsx(Typography, {
|
|
379
|
+
variant: "body2",
|
|
380
|
+
color: "text.secondary",
|
|
381
|
+
children: review.comment
|
|
382
|
+
}) : null,
|
|
383
|
+
uid && review.$id !== uid ? /*#__PURE__*/ _jsx(ReportTarget, {
|
|
384
|
+
listingId: listingId,
|
|
385
|
+
reviewUid: review.$id,
|
|
386
|
+
label: `this review by ${name}`
|
|
387
|
+
}) : null
|
|
388
|
+
]
|
|
389
|
+
})
|
|
390
|
+
]
|
|
391
|
+
}, review.$id);
|
|
392
|
+
})
|
|
393
|
+
}),
|
|
394
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
395
|
+
page: page,
|
|
396
|
+
pageSize: pageSize,
|
|
397
|
+
rowCount: visibleReviews.length,
|
|
398
|
+
// The reviews this card HOLDS, after the hidden ones are
|
|
399
|
+
// dropped — a slice of rows already read, so the total is
|
|
400
|
+
// known exactly for the window. The alert below is what says
|
|
401
|
+
// when the window itself is short of the listing.
|
|
402
|
+
count: reviews.length,
|
|
403
|
+
onPageChange: setPage,
|
|
404
|
+
onPageSizeChange: setPageSize
|
|
405
|
+
}),
|
|
406
|
+
reviewsTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
407
|
+
severity: "info",
|
|
408
|
+
children: `Showing ${REVIEW_CEILING} reviews, ordered by author. This ` + 'listing has more, and the rating summary above counts all ' + 'of them.'
|
|
409
|
+
}) : null
|
|
410
|
+
]
|
|
411
|
+
}) : null
|
|
412
|
+
]
|
|
413
|
+
})
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
ListingReviews.displayName = 'ListingReviews';
|
|
417
|
+
export default ListingReviews;
|
|
418
|
+
|
|
419
|
+
//# sourceMappingURL=listing-reviews.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/listing-reviews.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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Avatar,\n Button,\n Chip,\n Divider,\n Rating,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport ReportTarget from './report-target.component'\n\n/**\n * How many review documents this card reads.\n *\n * A CEILING, not a page size. Three things below are computed over the whole\n * set — the hidden-review filter, the newest-first sort, and finding the\n * reader's OWN review to seed the form — so a server page would be a page of\n * candidates and a reader whose review sat past the first ten would be shown\n * an empty form over a review they had already written.\n */\nconst REVIEW_CEILING = 200\n\n/**\n * Ratings and comments on a listing (AGL-655).\n *\n * Reads the subcollection directly — it is public-read, so there is no\n * value in proxying it through an API. Writes go through\n * `/api/marketplace/reviews`, which owns every rule that matters: only\n * accounts that installed the listing may rate it, the publishing org\n * cannot review itself, and the aggregates it maintains are frozen from\n * client writes.\n *\n * The star input is offered to everyone and refused server-side rather than\n * hidden, because \"you must install this to rate it\" is worth saying out\n * loud — a control that silently does nothing teaches nothing.\n */\nexport function ListingReviews({\n listingId,\n listing,\n}: {\n listingId: string\n listing: Record<string, any> | undefined\n}) {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const uid = (user as any)?.uid as string | undefined\n // Rating (not commenting) requires a verified email (AGL-865). We can't see\n // the install pins from here, so the \"installed\" half is still enforced\n // server-side; the email half we can reflect in the control's state.\n const emailVerified = Boolean((user as any)?.emailVerified)\n\n const { data: reviewDocs } = useFirestoreCollection<any>(\n /*\n * ORDERED AND CEILINGED, with a probe (AGL-2501).\n *\n * `limit(100)` alone is answered in DOCUMENT-ID order, and a review's\n * document id is its author's uid — so the hundred were a pseudo-random\n * hundred of the reviewers, and on a popular listing the reviews past\n * them were not merely unrendered but unreachable, because nothing drew\n * them and no control asked for more.\n *\n * Ordering on the document NAME changes which rows come back only in the\n * sense that the walk is now total: every reviewer is reachable by\n * paging. It is not chronological and nothing here claims it is — the\n * sort below puts the newest first, which it may do because it holds the\n * whole ceiling rather than a slice of it.\n *\n * `orderBy('updatedAtMs')` would have been the tempting fix and is the\n * dangerous one: it matches only documents that HAVE the field, so a\n * review written before that field existed would vanish from a listing's\n * page rather than sort oddly on it.\n */\n () =>\n collectionCeiling(\n collection(firestore, 'marketplaceListings', listingId, 'reviews'),\n REVIEW_CEILING,\n ),\n [firestore, listingId],\n { idField: '$id' },\n )\n const { rows: readReviews, truncated: reviewsTruncated } = ceilingedWindow<any>(\n reviewDocs,\n REVIEW_CEILING,\n )\n\n const reviews = useMemo(\n () =>\n [...readReviews]\n .filter((entry: any) => !entry.hidden)\n .sort((a: any, b: any) => (b.updatedAtMs ?? 0) - (a.updatedAtMs ?? 0)),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [reviewDocs],\n )\n const mine = useMemo(\n () => reviews.find((entry: any) => entry.$id === uid),\n [reviews, uid],\n )\n\n /*\n * The page is a SLICE of what the card already holds, for the three reasons\n * the ceiling exists: `hidden` is filtered after reading, the order on\n * screen is not the order the query walks, and `mine` seeds the form above\n * from anywhere in the set.\n */\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const visibleReviews = useMemo(\n () => reviews.slice(page * pageSize, page * pageSize + pageSize),\n [reviews, page, pageSize],\n )\n /*\n * A new listing starts at page one. Page four of a listing with two pages\n * does not exist, and an out-of-range page renders as an empty list with\n * nothing saying why — which reads as the reviews having gone.\n */\n useEffect(() => {\n setPage(0)\n }, [listingId])\n\n const [rating, setRating] = useState<number | null>(null)\n const [comment, setComment] = useState('')\n const [busy, setBusy] = useState(false)\n\n useEffect(() => {\n setRating(mine?.rating ?? null)\n setComment(mine?.comment ?? '')\n }, [mine])\n\n const submit = useCallback(async () => {\n if (busy || (!rating && !comment.trim())) return\n setBusy(true)\n try {\n const response = await authorizedFetch(user, '/api/marketplace/reviews', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ listingId, rating, comment: comment.trim() }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // 403 here is usually \"you have not installed this\" — actionable,\n // so it reads as guidance rather than an error.\n return void enqueueSnackbar(payload?.error ?? 'Could not post that', {\n variant: response.status === 403 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(mine ? 'Updated your review' : 'Thanks for the review', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not post that', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, rating, comment, user, listingId, mine, enqueueSnackbar])\n\n const remove = useCallback(async () => {\n if (busy || !mine) return\n setBusy(true)\n try {\n await authorizedFetch(user, '/api/marketplace/reviews', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ listingId }),\n })\n setRating(null)\n setComment('')\n enqueueSnackbar('Removed your review', {\n variant: 'success',\n persist: false,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, mine, user, listingId, enqueueSnackbar])\n\n const average = Number(listing?.ratingAverage ?? 0)\n const ratingCount = Number(listing?.ratingCount ?? 0)\n\n return (\n <CardDisplay\n header={'Ratings & comments'}\n help={pluginDocsHelp('installYourFirstPlugin', {\n anchor: '#step-3-reviews',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {ratingCount ? (\n <>\n <Rating value={average} precision={0.1} size=\"small\" readOnly />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${average} · ${ratingCount} rating${\n ratingCount === 1 ? '' : 's'\n }`}\n </Typography>\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No ratings yet.'}\n </Typography>\n )}\n </Stack>\n\n {uid ? (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">\n {mine ? 'Your review' : 'Leave a review'}\n </Typography>\n {/* Rating and comment are distinct affordances (AGL-865): a rating\n is gated on a verified email + an install and moves the score;\n a comment is open to anyone signed in. */}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" sx={{ minWidth: 56 }}>\n {'Rating'}\n </Typography>\n <Rating\n value={rating}\n size=\"small\"\n onChange={(_event, value) => setRating(value)}\n disabled={busy || !emailVerified}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {emailVerified\n ? 'Requires having installed this'\n : 'Verify your email to rate'}\n </Typography>\n </Stack>\n <TextField\n size=\"small\"\n label=\"Comment\"\n placeholder=\"Share how it worked out, or ask a question — anyone can comment\"\n value={comment}\n onChange={(event) => setComment(event.target.value)}\n disabled={busy}\n multiline\n minRows={2}\n fullWidth\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || (!rating && !comment.trim())}\n onClick={() => void submit()}\n >\n {busy ? 'Posting…' : mine ? 'Update' : 'Post'}\n </Button>\n {mine ? (\n <Button size=\"small\" disabled={busy} onClick={() => void remove()}>\n {'Remove'}\n </Button>\n ) : null}\n </Stack>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sign in to leave a rating or comment.'}\n </Typography>\n )}\n\n {reviews.length ? (\n <>\n <Divider />\n <Stack spacing={2}>\n {visibleReviews.map((review: any) => {\n const name = String(review.displayName ?? 'Someone')\n return (\n <Stack\n key={review.$id}\n direction=\"row\"\n spacing={1.5}\n sx={{ alignItems: 'flex-start' }}\n >\n <Avatar\n sx={{ width: 32, height: 32, fontSize: 14 }}\n >\n {name.trim().charAt(0).toUpperCase() || '?'}\n </Avatar>\n <Stack spacing={0.25} sx={{ flex: 1, minWidth: 0 }}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {name}\n </Typography>\n {/* Server-set, so the badge means something: it is the\n difference between \"used it\" and \"walked past it\". */}\n {review.verifiedInstaller ? (\n <Chip\n size=\"small\"\n label=\"Installed\"\n color=\"primary\"\n />\n ) : null}\n {review.updatedAtMs ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n >\n {new Date(review.updatedAtMs).toLocaleDateString()}\n </Typography>\n ) : null}\n </Stack>\n {review.rating ? (\n <Rating value={review.rating} size=\"small\" readOnly />\n ) : null}\n {review.comment ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {review.comment}\n </Typography>\n ) : null}\n {/* Not on your OWN review: the queue is for things a\n stranger wrote, and \"report yourself\" is noise a\n staff member then has to read. Editing yours is\n already the control above. */}\n {uid && review.$id !== uid ? (\n <ReportTarget\n listingId={listingId}\n reviewUid={review.$id}\n label={`this review by ${name}`}\n />\n ) : null}\n </Stack>\n </Stack>\n )\n })}\n </Stack>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={visibleReviews.length}\n // The reviews this card HOLDS, after the hidden ones are\n // dropped — a slice of rows already read, so the total is\n // known exactly for the window. The alert below is what says\n // when the window itself is short of the listing.\n count={reviews.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {reviewsTruncated ? (\n <Alert severity=\"info\">\n {`Showing ${REVIEW_CEILING} reviews, ordered by author. This ` +\n 'listing has more, and the rating summary above counts all ' +\n 'of them.'}\n </Alert>\n ) : null}\n </>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\n\nListingReviews.displayName = 'ListingReviews'\n\nexport default ListingReviews\n"],"names":["CardDisplay","useSnackbar","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","useUser","Alert","Avatar","Button","Chip","Divider","Rating","Stack","TextField","Typography","collection","useCallback","useEffect","useMemo","useState","ListPagination","TABLE_PAGE_SIZE_DEFAULT","pluginDocsHelp","authorizedFetch","ReportTarget","REVIEW_CEILING","ListingReviews","listingId","listing","firestore","data","user","enqueueSnackbar","uid","emailVerified","Boolean","reviewDocs","idField","rows","readReviews","truncated","reviewsTruncated","reviews","filter","entry","hidden","sort","a","b","updatedAtMs","mine","find","$id","page","setPage","pageSize","setPageSize","visibleReviews","slice","rating","setRating","comment","setComment","busy","setBusy","submit","trim","response","method","headers","body","JSON","stringify","payload","json","catch","ok","error","variant","status","allowDuplicate","persist","console","remove","average","Number","ratingAverage","ratingCount","header","help","anchor","contentGutterX","contentGutterY","spacing","direction","sx","alignItems","value","precision","size","readOnly","color","minWidth","onChange","_event","disabled","label","placeholder","event","target","multiline","minRows","fullWidth","onClick","length","map","review","name","String","displayName","width","height","fontSize","charAt","toUpperCase","flex","flexWrap","fontWeight","verifiedInstaller","Date","toLocaleDateString","reviewUid","rowCount","count","onPageChange","onPageSizeChange","severity"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type OrgPermissions } from '@aglyn/aglyn';
|
|
2
|
+
export interface MarketplaceBrowseProps {
|
|
3
|
+
hostId: string;
|
|
4
|
+
/** Signed-in user's org permissions, supplied by the shell (AGL-395). */
|
|
5
|
+
permissions?: Partial<OrgPermissions>;
|
|
6
|
+
/**
|
|
7
|
+
* Rendered inside the org-scope `/marketplace` route (AGL-772) rather
|
|
8
|
+
* than a site's marketplace tab. Only affects link targets — the grid
|
|
9
|
+
* still installs through the acting `hostId` until targeting lands
|
|
10
|
+
* (AGL-773) — so detail links resolve to the org route, not a per-site
|
|
11
|
+
* one that is being retired.
|
|
12
|
+
*/
|
|
13
|
+
orgScoped?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The acting org's slug from the URL (AGL-867). When given at org scope,
|
|
16
|
+
* detail links build from it directly instead of the async
|
|
17
|
+
* `hostIndex`→`orgs` resolution, which can return empty and leave the detail
|
|
18
|
+
* page — the only place installs happen now — unreachable from browse.
|
|
19
|
+
*/
|
|
20
|
+
orgSlug?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Restrict the grid to one publisher's listings (AGL-869), for the org-scope
|
|
23
|
+
* publisher page. Omitted on the main browse, which shows everyone.
|
|
24
|
+
*/
|
|
25
|
+
publisherId?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Marketplace components browse (AGL-44). A read-only catalogue: each card links
|
|
29
|
+
* to the listing's detail page, which is the only place an install happens
|
|
30
|
+
* (AGL-867) — installing from a grid card was too easy and skipped the
|
|
31
|
+
* site-targeting choice. The card still shows install STATE (installed,
|
|
32
|
+
* org-wide) so the shelf reads honestly, but carries no install action.
|
|
33
|
+
*/
|
|
34
|
+
export declare function MarketplaceBrowse(props: MarketplaceBrowseProps): import("react").JSX.Element;
|
|
35
|
+
export declare namespace MarketplaceBrowse {
|
|
36
|
+
var displayName: string;
|
|
37
|
+
}
|
|
38
|
+
export default MarketplaceBrowse;
|