@mohasinac/appkit 4.32.0 → 4.32.2
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/dist/_internal/server/features/bundles/data.js +13 -6
- package/dist/_internal/server/features/stores/data.js +6 -3
- package/dist/features/account/components/UserBidsView.js +6 -1
- package/dist/features/account/components/UserOrdersView.js +8 -1
- package/dist/features/account/components/UserReturnsView.js +6 -1
- package/dist/features/admin/components/AdminAuditLogView.js +12 -1
- package/dist/features/admin/components/AdminBundlesView.js +5 -1
- package/dist/features/admin/components/AdminFaqsView.js +10 -1
- package/dist/features/admin/components/AdminNotificationsView.js +7 -1
- package/dist/features/admin/components/AdminPrizeDrawsView.js +11 -1
- package/dist/features/admin/components/AdminStoresView.js +13 -1
- package/dist/features/admin/components/AdminSupportTicketsView.js +11 -1
- package/dist/features/admin/components/AdminTeamView.js +11 -1
- package/dist/features/admin/components/AdminTesterChecklistView.js +6 -1
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +6 -1
- package/dist/features/auctions/actions/bid-actions.js +4 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +5 -3
- package/dist/features/blog/components/BlogIndexPageView.js +2 -3
- package/dist/features/categories/components/BundlesListView.js +5 -3
- package/dist/features/reviews/components/ReviewsIndexPageView.js +2 -3
- package/dist/features/seller/components/SellerBundlesView.js +5 -1
- package/dist/features/seller/components/SellerProductsView.js +10 -1
- package/dist/features/stores/actions/store-query-actions.js +5 -3
- package/dist/features/stores/components/StoreBundlesPageView.js +7 -3
- package/dist/features/stores/components/StoresIndexPageView.js +2 -3
- package/dist/features/stores/repository/store.repository.d.ts +3 -1
- package/dist/features/stores/repository/store.repository.js +6 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +12 -0
- package/package.json +1 -1
|
@@ -149,9 +149,11 @@ export async function resolveBundleCategorySlugs(productIds) {
|
|
|
149
149
|
*/
|
|
150
150
|
export const listFeaturedBundles = cache(async (limit = 8, _opts) => {
|
|
151
151
|
void _opts;
|
|
152
|
-
const all = await categoriesRepository
|
|
153
|
-
|
|
154
|
-
.
|
|
152
|
+
const all = await safeRead(() => categoriesRepository.listByType("bundle", { activeOnly: true, limit }), {
|
|
153
|
+
route: "bundles/getFeaturedBundles",
|
|
154
|
+
key: "categories.listByType(bundle)",
|
|
155
|
+
fallback: [],
|
|
156
|
+
});
|
|
155
157
|
return all;
|
|
156
158
|
});
|
|
157
159
|
/**
|
|
@@ -163,8 +165,13 @@ export const listFeaturedBundles = cache(async (limit = 8, _opts) => {
|
|
|
163
165
|
* detail pages had no related section at all before this.
|
|
164
166
|
*/
|
|
165
167
|
export const getRelatedBundles = cache(async (bundle, limit = 8) => {
|
|
166
|
-
const all = await categoriesRepository
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
const all = await safeRead(() => categoriesRepository.listByType("bundle", {
|
|
169
|
+
activeOnly: true,
|
|
170
|
+
limit: limit + 1,
|
|
171
|
+
}), {
|
|
172
|
+
route: "bundles/getRelatedBundles",
|
|
173
|
+
key: "categories.listByType(bundle)",
|
|
174
|
+
fallback: [],
|
|
175
|
+
});
|
|
169
176
|
return all.filter((b) => b.id !== bundle.id).slice(0, limit);
|
|
170
177
|
});
|
|
@@ -3,6 +3,7 @@ import { cache } from "react";
|
|
|
3
3
|
import { storeRepository } from "../../../../repositories";
|
|
4
4
|
import { STORES_PRODUCTS_PAGE_SIZE, STORES_SITEMAP_LIMIT, } from "../../../shared/features/stores/config";
|
|
5
5
|
import { makeGetStoreListingsInitial } from "../shared/listing-data-factory";
|
|
6
|
+
import { safeRead } from "../../../../errors/safe-read";
|
|
6
7
|
/** Full store document by slug — deduped per request via React.cache(). */
|
|
7
8
|
export const getStoreForDetail = cache(async (slug) => {
|
|
8
9
|
return (await storeRepository.findBySlug(slug)) ?? null;
|
|
@@ -13,8 +14,10 @@ export const listStoreAuctionsInitial = makeGetStoreListingsInitial("auction", S
|
|
|
13
14
|
export const listStorePreOrdersInitial = makeGetStoreListingsInitial("pre-order", STORES_PRODUCTS_PAGE_SIZE);
|
|
14
15
|
/** Stores for sitemap generation. */
|
|
15
16
|
export const listSitemapStores = cache(async () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/*
|
|
18
|
+
* The SITEMAP. A swallowed failure here publishes a sitemap missing every
|
|
19
|
+
* store — which is not an error to any crawler, just a smaller site.
|
|
20
|
+
*/
|
|
21
|
+
const result = await safeRead(() => storeRepository.listStores({ filters: "status==active", sorts: "-updatedAt", page: 1, pageSize: STORES_SITEMAP_LIMIT }, true), { route: "sitemap/stores", key: "stores.listStores", fallback: null });
|
|
19
22
|
return (result?.items ?? []).map(({ storeSlug, updatedAt }) => ({ storeSlug, updatedAt }));
|
|
20
23
|
});
|
|
@@ -31,7 +31,12 @@ export function UserBidsView() {
|
|
|
31
31
|
const config = {
|
|
32
32
|
portal: "user",
|
|
33
33
|
title: "My Bids",
|
|
34
|
-
|
|
34
|
+
search: {
|
|
35
|
+
placeholder: "Search by auction title…",
|
|
36
|
+
// `/api/user/bids` filters in memory: `(b.productTitle ?? "").includes(q)`.
|
|
37
|
+
// The bid document carries no other text.
|
|
38
|
+
fields: ["productTitle"],
|
|
39
|
+
},
|
|
35
40
|
emptyLabel: "You haven't placed any bids yet.",
|
|
36
41
|
filterKeys: ["status"],
|
|
37
42
|
defaultSort: sortBy("bidDate", "DESC"),
|
|
@@ -48,7 +48,14 @@ export function UserOrdersView({ onOrderClick }) {
|
|
|
48
48
|
const config = {
|
|
49
49
|
portal: "user",
|
|
50
50
|
title: "My Orders",
|
|
51
|
-
|
|
51
|
+
search: {
|
|
52
|
+
placeholder: "Search by order id…",
|
|
53
|
+
// `/api/user/orders` filters in memory on `o.id` and nothing else — not
|
|
54
|
+
// the product titles the rows display, which is what a buyer would try
|
|
55
|
+
// first. Saying "order id" is the honest promise until the route can
|
|
56
|
+
// match more.
|
|
57
|
+
fields: ["id"],
|
|
58
|
+
},
|
|
52
59
|
emptyLabel: "You haven't placed any orders yet.",
|
|
53
60
|
filterKeys: ["status", "orderType"],
|
|
54
61
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
@@ -21,7 +21,12 @@ export function UserReturnsView({ onOrderClick }) {
|
|
|
21
21
|
const config = {
|
|
22
22
|
portal: "user",
|
|
23
23
|
title: "Returns & Refunds",
|
|
24
|
-
|
|
24
|
+
search: {
|
|
25
|
+
placeholder: "Search by order id…",
|
|
26
|
+
// Same route as /user/orders, scoped to returned statuses — so the same
|
|
27
|
+
// single in-memory match on `o.id`.
|
|
28
|
+
fields: ["id"],
|
|
29
|
+
},
|
|
25
30
|
emptyLabel: "You have no return or refund requests.",
|
|
26
31
|
filterKeys: [],
|
|
27
32
|
defaultSort: sortBy("updatedAt", "DESC"),
|
|
@@ -50,7 +50,18 @@ export function AdminAuditLogView({ children, ...props }) {
|
|
|
50
50
|
const config = {
|
|
51
51
|
portal: "admin",
|
|
52
52
|
title: "Audit Log",
|
|
53
|
-
|
|
53
|
+
search: {
|
|
54
|
+
placeholder: "Search by actor uid (exact)",
|
|
55
|
+
/*
|
|
56
|
+
* `/api/admin/audit-log` builds `actorUid==${q}` — an EQUALITY, stated in
|
|
57
|
+
* that route's own comment because `@=` is prefix-only against Firestore
|
|
58
|
+
* anyway. A partial uid matches nothing, so the box commits on Enter and
|
|
59
|
+
* says "exact" rather than letting someone type half a uid and watch an
|
|
60
|
+
* empty list.
|
|
61
|
+
*/
|
|
62
|
+
mode: "exact",
|
|
63
|
+
fields: ["actorUid"],
|
|
64
|
+
},
|
|
54
65
|
emptyLabel: "No audit log entries found",
|
|
55
66
|
filterKeys: ["action", "actorUid"],
|
|
56
67
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
@@ -62,7 +62,11 @@ export function AdminBundlesView({ editHrefTemplate, newHref }) {
|
|
|
62
62
|
const config = {
|
|
63
63
|
portal: "admin",
|
|
64
64
|
title: "Bundles",
|
|
65
|
-
|
|
65
|
+
search: {
|
|
66
|
+
placeholder: "Search bundles by name or slug…",
|
|
67
|
+
// `/api/admin/bundles` filters in memory on `b.name` and `b.slug`.
|
|
68
|
+
fields: ["name", "slug"],
|
|
69
|
+
},
|
|
66
70
|
emptyLabel: BUNDLE_COPY.adminList.empty,
|
|
67
71
|
filterKeys: ["isActive", "bundleStockStatus"],
|
|
68
72
|
defaultSort: sortBy("name", "ASC"),
|
|
@@ -33,7 +33,16 @@ export function AdminFaqsView({ children, onBulkArchive, onBulkDelete, ...props
|
|
|
33
33
|
const config = useMemo(() => ({
|
|
34
34
|
portal: "admin",
|
|
35
35
|
title: "FAQs",
|
|
36
|
-
|
|
36
|
+
search: {
|
|
37
|
+
placeholder: "Search questions, answers, categories or tags",
|
|
38
|
+
/*
|
|
39
|
+
* `buildFaqSearchTxt` indexes the question, the category, every tag AND
|
|
40
|
+
* the answer body, all prefix-expanded. The old placeholder said
|
|
41
|
+
* "tokens" — the name of the implementation — and omitted the answer,
|
|
42
|
+
* which is the largest thing it searches.
|
|
43
|
+
*/
|
|
44
|
+
fields: ["question", "answer", "category", "tags"],
|
|
45
|
+
},
|
|
37
46
|
emptyLabel: "No FAQs found",
|
|
38
47
|
filterKeys: ["isActive"],
|
|
39
48
|
defaultSort: sortBy("priority", "ASC"),
|
|
@@ -81,7 +81,13 @@ export function AdminNotificationsView({ children, ...props }) {
|
|
|
81
81
|
// Title search is not expressible here — `title` is not in the repository's
|
|
82
82
|
// SIEVE_FIELDS and Sieve→Firestore cannot OR across two fields. The route
|
|
83
83
|
// treats `q` as an exact userId lookup, so the placeholder says exactly that.
|
|
84
|
-
|
|
84
|
+
search: {
|
|
85
|
+
placeholder: "Search by user ID (exact)",
|
|
86
|
+
// `/api/admin/notifications` builds `userId==${q}`. An equality, so a
|
|
87
|
+
// partial id matches nothing and the box commits on Enter.
|
|
88
|
+
mode: "exact",
|
|
89
|
+
fields: ["userId"],
|
|
90
|
+
},
|
|
85
91
|
emptyLabel: "No notifications found",
|
|
86
92
|
filterKeys: ["type", "readState"],
|
|
87
93
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
@@ -75,7 +75,17 @@ export function AdminPrizeDrawsView({ children, ...props }) {
|
|
|
75
75
|
const config = {
|
|
76
76
|
portal: "admin",
|
|
77
77
|
title: "Prize Draws",
|
|
78
|
-
|
|
78
|
+
search: {
|
|
79
|
+
placeholder: "Search prize draws by name, brand or tag…",
|
|
80
|
+
/*
|
|
81
|
+
* 🛑 "or store" was a promise the corpus cannot keep.
|
|
82
|
+
* `buildProductSearchTxt` indexes title, description, brand, brandSlug,
|
|
83
|
+
* category names, tags, features, condition and card/grading details —
|
|
84
|
+
* and NO store name or id. Typing a seller's name returned an empty list
|
|
85
|
+
* that reads as "this seller runs no prize draws".
|
|
86
|
+
*/
|
|
87
|
+
fields: ["title", "description", "brand", "categoryNames", "tags", "features"],
|
|
88
|
+
},
|
|
79
89
|
emptyLabel: "No prize draws found",
|
|
80
90
|
filterKeys: ["status"],
|
|
81
91
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
@@ -43,7 +43,19 @@ export function AdminStoresView({ children, ...props }) {
|
|
|
43
43
|
const config = {
|
|
44
44
|
portal: "admin",
|
|
45
45
|
title: "Stores",
|
|
46
|
-
|
|
46
|
+
search: {
|
|
47
|
+
placeholder: "Search stores by name, description or category",
|
|
48
|
+
/*
|
|
49
|
+
* 🛑 "slugs, or owner names" was a promise nothing could keep — neither
|
|
50
|
+
* is in `buildStoreSearchTxt`, and the owner's name lives on another
|
|
51
|
+
* collection entirely.
|
|
52
|
+
*
|
|
53
|
+
* The route now searches `searchTxt` rather than `storeName_=`, so this
|
|
54
|
+
* is genuinely partial across all three fields; it used to be a
|
|
55
|
+
* STARTS-WITH match on the name alone.
|
|
56
|
+
*/
|
|
57
|
+
fields: ["storeName", "storeDescription", "storeCategory"],
|
|
58
|
+
},
|
|
47
59
|
emptyLabel: "No stores found",
|
|
48
60
|
filterKeys: ["status"],
|
|
49
61
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
@@ -57,7 +57,17 @@ export function AdminSupportTicketsView({ children, ...props }) {
|
|
|
57
57
|
const config = {
|
|
58
58
|
portal: "admin",
|
|
59
59
|
title: "Support Tickets",
|
|
60
|
-
|
|
60
|
+
search: {
|
|
61
|
+
placeholder: "Search by subject (starts with)",
|
|
62
|
+
/*
|
|
63
|
+
* `/api/admin/support-tickets` builds `subject@=${q}`, and `@=` is
|
|
64
|
+
* PREFIX-only against Firestore. `searchTxt` on this collection is built
|
|
65
|
+
* from the subject alone on purpose — the description and every message
|
|
66
|
+
* body are PII-encrypted, and indexing them in plaintext would hand back
|
|
67
|
+
* exactly what the encryption withholds.
|
|
68
|
+
*/
|
|
69
|
+
fields: ["subject"],
|
|
70
|
+
},
|
|
61
71
|
emptyLabel: "No support tickets found",
|
|
62
72
|
filterKeys: ["status", "priority", "showResolved"],
|
|
63
73
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
@@ -57,7 +57,17 @@ export function AdminTeamView({ children, onBulkRemove, ...props }) {
|
|
|
57
57
|
const config = {
|
|
58
58
|
portal: "admin",
|
|
59
59
|
title: "Team",
|
|
60
|
-
|
|
60
|
+
search: {
|
|
61
|
+
placeholder: "Search by name or email",
|
|
62
|
+
/*
|
|
63
|
+
* Genuinely partial, and that is worth stating: `/api/admin/team`
|
|
64
|
+
* filters AFTER `mapDoc` has decrypted, with
|
|
65
|
+
* `(u.email ?? "").toLowerCase().includes(q)`. Most PII search in this
|
|
66
|
+
* codebase resolves an HMAC blind index and matches exactly; this one
|
|
67
|
+
* does not, which is why `mode` stays partial here.
|
|
68
|
+
*/
|
|
69
|
+
fields: ["displayName", "email"],
|
|
70
|
+
},
|
|
61
71
|
emptyLabel: "No employees found",
|
|
62
72
|
filterKeys: ["group"],
|
|
63
73
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
@@ -54,7 +54,12 @@ export function AdminTesterChecklistView({ children, onBulkDelete, ...props }) {
|
|
|
54
54
|
const config = useMemo(() => ({
|
|
55
55
|
portal: "admin",
|
|
56
56
|
title: "Tester Checklist",
|
|
57
|
-
|
|
57
|
+
search: {
|
|
58
|
+
placeholder: "Search test cases, descriptions, groups or pages",
|
|
59
|
+
// `TesterChecklistItemRepository` builds `searchTxt` from label,
|
|
60
|
+
// description, groupLabel and pageLabel.
|
|
61
|
+
fields: ["label", "description", "groupLabel", "pageLabel"],
|
|
62
|
+
},
|
|
58
63
|
emptyLabel: "No checklist items found",
|
|
59
64
|
filterKeys: ["isActive", "bugConfirmed"],
|
|
60
65
|
// Bug-confirmed (and reopened-away) cases are always isActive:false —
|
|
@@ -33,7 +33,12 @@ export function AdminPageViewsReportView() {
|
|
|
33
33
|
const config = {
|
|
34
34
|
portal: "admin",
|
|
35
35
|
title: "Page Views",
|
|
36
|
-
|
|
36
|
+
search: {
|
|
37
|
+
placeholder: "Search by entity or URL",
|
|
38
|
+
// `/api/admin/analytics/pageviews` filters in memory on `row.entityId`
|
|
39
|
+
// and `row.url`.
|
|
40
|
+
fields: ["entityId", "url"],
|
|
41
|
+
},
|
|
37
42
|
emptyLabel: "No page views recorded for this range.",
|
|
38
43
|
filterKeys: ["entityType"],
|
|
39
44
|
defaultSort: sortBy("count", "DESC"),
|
|
@@ -28,6 +28,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
|
28
28
|
import { cartRepository } from "../../cart/repository/cart.repository";
|
|
29
29
|
import { AUCTION_BUYOUT_WINDOW_MINUTES, AUCTION_BUYOUT_WINDOW_MS, AUCTION_CHECKOUT_URL, } from "../../../_internal/shared/checkout/lanes";
|
|
30
30
|
import { resolveDate } from "../../../utils";
|
|
31
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
31
32
|
// --- Domain Functions ----------------------------------------------------------
|
|
32
33
|
/**
|
|
33
34
|
* Load the bidder's profile and refuse if they carry an active `place_bids`
|
|
@@ -110,9 +111,9 @@ export async function placeBid(userId, userEmail, input) {
|
|
|
110
111
|
: BID_ERROR_CODES.TOO_LOW,
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// A failure here is not "no previous winner" — it silently uncaps the
|
|
115
|
+
// auto-bid ceiling below, which is a money path.
|
|
116
|
+
const previousWinner = await safeRead(() => bidRepository.getWinningBid(productId), { route: "bid-actions/placeBid", key: "bids.getWinningBid", fallback: null });
|
|
116
117
|
const prevCap = previousWinner?.data
|
|
117
118
|
? (previousWinner.data.autoMaxBid ?? previousWinner.data.bidAmount ?? baseBid)
|
|
118
119
|
: baseBid;
|
|
@@ -132,9 +132,11 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
132
132
|
: [];
|
|
133
133
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
134
134
|
const sublistingCategoryId = typeof p.sublistingCategoryId === "string" ? p.sublistingCategoryId : null;
|
|
135
|
-
const relatedDocs = await productRepository
|
|
136
|
-
|
|
137
|
-
.
|
|
135
|
+
const relatedDocs = (await safeRead(() => productRepository.findByCategory(String(p.category ?? "")), {
|
|
136
|
+
route: "/auctions/[slug]",
|
|
137
|
+
key: "products.findByCategory",
|
|
138
|
+
fallback: [],
|
|
139
|
+
}));
|
|
138
140
|
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, siteSettings] = await Promise.all([
|
|
139
141
|
computeRelatedItems(product),
|
|
140
142
|
getGroupsWithItemsForProduct(product.id),
|
|
@@ -3,6 +3,7 @@ import { blogRepository } from "../../../repositories";
|
|
|
3
3
|
import { Container, Heading, Main, Section } from "../../../ui";
|
|
4
4
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
5
5
|
import { BlogIndexListing } from "./BlogIndexListing";
|
|
6
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
6
7
|
function sp(params, key) {
|
|
7
8
|
const v = params[key];
|
|
8
9
|
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
@@ -12,9 +13,7 @@ export async function BlogIndexPageView({ searchParams = {} }) {
|
|
|
12
13
|
const page = Number(sp(searchParams, "page")) || 1;
|
|
13
14
|
const pageSize = Number(sp(searchParams, "pageSize")) || 24;
|
|
14
15
|
const category = sp(searchParams, "category");
|
|
15
|
-
const sieveResult = await blogRepository
|
|
16
|
-
.listPublished({ ...(category ? { category } : {}) }, { sorts: sort, page, pageSize })
|
|
17
|
-
.catch(() => null);
|
|
16
|
+
const sieveResult = await safeRead(() => blogRepository.listPublished({ ...(category ? { category } : {}) }, { sorts: sort, page, pageSize }), { route: "/blog", key: "blogPosts.listPublished", fallback: null });
|
|
18
17
|
const initialData = sieveResult
|
|
19
18
|
? {
|
|
20
19
|
posts: sieveResult.items,
|
|
@@ -3,6 +3,7 @@ import { categoriesRepository } from "../../../repositories";
|
|
|
3
3
|
import { Container, Heading, Main, Section, Text } from "../../../ui";
|
|
4
4
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
5
5
|
import { CategoryBundlesListing } from "./CategoryBundlesListing";
|
|
6
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
6
7
|
const DEFAULT_LIMIT = 200;
|
|
7
8
|
function sp(params, key) {
|
|
8
9
|
const v = params[key];
|
|
@@ -17,9 +18,10 @@ function sp(params, key) {
|
|
|
17
18
|
*/
|
|
18
19
|
export async function BundlesListView({ searchParams = {}, onBuyNow, }) {
|
|
19
20
|
const storeId = sp(searchParams, "storeId");
|
|
20
|
-
const all = await categoriesRepository
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const all = await safeRead(() => categoriesRepository.listByType("bundle", {
|
|
22
|
+
activeOnly: true,
|
|
23
|
+
limit: DEFAULT_LIMIT,
|
|
24
|
+
}), { route: "/bundles", key: "categories.listByType(bundle)", fallback: [] });
|
|
23
25
|
const bundles = storeId
|
|
24
26
|
? all.filter((c) => c.createdByStoreId === storeId)
|
|
25
27
|
: all;
|
|
@@ -4,6 +4,7 @@ import { reviewRepository } from "../../../repositories";
|
|
|
4
4
|
import { Container, Heading, Main, Section } from "../../../ui";
|
|
5
5
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
6
6
|
import { ReviewsIndexListing } from "./ReviewsIndexListing";
|
|
7
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
7
8
|
function sp(params, key) {
|
|
8
9
|
const v = params[key];
|
|
9
10
|
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
@@ -39,9 +40,7 @@ export async function ReviewsIndexPageView({ searchParams = {} }) {
|
|
|
39
40
|
const sort = sp(searchParams, "sort") || "-createdAt";
|
|
40
41
|
const page = Number(sp(searchParams, "page")) || 1;
|
|
41
42
|
const filters = buildReviewFilters(searchParams);
|
|
42
|
-
const result = await reviewRepository
|
|
43
|
-
.listAll({ filters, sorts: sort, page, pageSize: 12 })
|
|
44
|
-
.catch(() => null);
|
|
43
|
+
const result = await safeRead(() => reviewRepository.listAll({ filters, sorts: sort, page, pageSize: 12 }), { route: "/reviews", key: "reviews.listAll", fallback: null });
|
|
45
44
|
const initialData = result
|
|
46
45
|
? {
|
|
47
46
|
items: result.items,
|
|
@@ -41,7 +41,11 @@ export function SellerBundlesView({ editHrefTemplate, newHref }) {
|
|
|
41
41
|
const config = {
|
|
42
42
|
portal: "seller",
|
|
43
43
|
title: "Bundles",
|
|
44
|
-
|
|
44
|
+
search: {
|
|
45
|
+
placeholder: "Search bundles by name or slug…",
|
|
46
|
+
// `/api/store/bundles` filters in memory on `b.name` and `b.slug`.
|
|
47
|
+
fields: ["name", "slug"],
|
|
48
|
+
},
|
|
45
49
|
emptyLabel: "No bundles yet — create a bundle to group multiple of your products together",
|
|
46
50
|
filterKeys: ["isActive", "bundleStockStatus"],
|
|
47
51
|
defaultSort: sortBy("name", "ASC"),
|
|
@@ -212,7 +212,16 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
212
212
|
const config = {
|
|
213
213
|
portal: "seller",
|
|
214
214
|
title: "Products",
|
|
215
|
-
|
|
215
|
+
search: {
|
|
216
|
+
placeholder: "Search by name, description, brand or tag…",
|
|
217
|
+
/*
|
|
218
|
+
* The box understated itself: `/api/store/products` routes `q` through
|
|
219
|
+
* `buildProductSearchTxt`, which indexes far more than the title. A
|
|
220
|
+
* seller searching a brand or a tag got results and had no reason to
|
|
221
|
+
* expect them.
|
|
222
|
+
*/
|
|
223
|
+
fields: ["title", "description", "brand", "categoryNames", "tags", "features"],
|
|
224
|
+
},
|
|
216
225
|
emptyLabel: listingKind !== "all" ? `No ${listingKind} listings found` : "No products listed yet",
|
|
217
226
|
filterKeys: FILTER_KEYS,
|
|
218
227
|
defaultSort: DEFAULT_SORT,
|
|
@@ -19,13 +19,15 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
|
19
19
|
export async function listStores(params = {}, viewer) {
|
|
20
20
|
const { filters, sorts = "-createdAt", page = 1, pageSize = 24, q } = params;
|
|
21
21
|
const model = { filters, sorts, page, pageSize };
|
|
22
|
+
// The same fix as the admin route: `q` travels as the searchTxt term, not as
|
|
23
|
+
// `storeName_=`, which was a prefix match on the name alone.
|
|
22
24
|
const filtersArr = [];
|
|
23
|
-
if (q)
|
|
24
|
-
filtersArr.push(`storeName_=${q}`);
|
|
25
25
|
if (filters)
|
|
26
26
|
filtersArr.push(filters);
|
|
27
27
|
model.filters = filtersArr.join(",") || undefined;
|
|
28
|
-
const result = await storeRepository.listStores(model
|
|
28
|
+
const result = await storeRepository.listStores(model, true, {
|
|
29
|
+
search: q || undefined,
|
|
30
|
+
});
|
|
29
31
|
return { ...result, items: filterTestDataForViewer(result.items, viewer) };
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { categoriesRepository } from "../../../repositories";
|
|
3
3
|
import { getStoreBySlug } from "./StoreDetailLayoutView";
|
|
4
4
|
import { CategoryBundlesListing } from "../../categories/components/CategoryBundlesListing";
|
|
5
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
5
6
|
/**
|
|
6
7
|
* Public store → Bundles tab.
|
|
7
8
|
*
|
|
@@ -16,9 +17,12 @@ export async function StoreBundlesPageView({ storeSlug, onBuyNow, }) {
|
|
|
16
17
|
if (!storeId) {
|
|
17
18
|
return null;
|
|
18
19
|
}
|
|
19
|
-
const bundles = await categoriesRepository
|
|
20
|
+
const bundles = await safeRead(() => categoriesRepository
|
|
20
21
|
.listByType("bundle", { activeOnly: true, limit: 50 })
|
|
21
|
-
.then((rows) => rows.filter((c) => c.createdByStoreId === storeId))
|
|
22
|
-
|
|
22
|
+
.then((rows) => rows.filter((c) => c.createdByStoreId === storeId)), {
|
|
23
|
+
route: "/stores/[storeSlug]/bundles",
|
|
24
|
+
key: "categories.listByType(bundle)",
|
|
25
|
+
fallback: [],
|
|
26
|
+
});
|
|
23
27
|
return _jsx(CategoryBundlesListing, { initialBundles: bundles, onBuyNow: onBuyNow });
|
|
24
28
|
}
|
|
@@ -3,6 +3,7 @@ import { storeRepository } from "../../../repositories";
|
|
|
3
3
|
import { Container, Heading, Main, Section } from "../../../ui";
|
|
4
4
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
5
5
|
import { StoresIndexListing } from "./StoresIndexListing";
|
|
6
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
6
7
|
function sp(params, key) {
|
|
7
8
|
const v = params[key];
|
|
8
9
|
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
@@ -11,8 +12,6 @@ export async function StoresIndexPageView({ searchParams = {} }) {
|
|
|
11
12
|
const sort = sp(searchParams, "sort") || "-createdAt";
|
|
12
13
|
const page = Number(sp(searchParams, "page")) || 1;
|
|
13
14
|
const pageSize = Number(sp(searchParams, "pageSize")) || 24;
|
|
14
|
-
const result = await storeRepository
|
|
15
|
-
.listStores({ page, pageSize, sorts: sort }, true)
|
|
16
|
-
.catch(() => null);
|
|
15
|
+
const result = await safeRead(() => storeRepository.listStores({ page, pageSize, sorts: sort }, true), { route: "/stores", key: "stores.listStores", fallback: null });
|
|
17
16
|
return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Stores" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(StoresIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
|
|
18
17
|
}
|
|
@@ -98,7 +98,9 @@ export declare class StoreRepository extends BaseRepository<StoreDocument> {
|
|
|
98
98
|
/**
|
|
99
99
|
* Admin: list all stores regardless of status.
|
|
100
100
|
*/
|
|
101
|
-
listAllStores(model: SieveModel
|
|
101
|
+
listAllStores(model: SieveModel, opts?: {
|
|
102
|
+
search?: string;
|
|
103
|
+
}): Promise<FirebaseSieveResult<StoreDocument>>;
|
|
102
104
|
/**
|
|
103
105
|
* Cloud Functions: atomically increment `stats.totalProducts` counter.
|
|
104
106
|
*/
|
|
@@ -230,8 +230,12 @@ export class StoreRepository extends BaseRepository {
|
|
|
230
230
|
/**
|
|
231
231
|
* Admin: list all stores regardless of status.
|
|
232
232
|
*/
|
|
233
|
-
async listAllStores(model) {
|
|
234
|
-
|
|
233
|
+
async listAllStores(model, opts) {
|
|
234
|
+
// 🛑 `opts` is forwarded. It was not, so the admin store list had no way to
|
|
235
|
+
// reach the `searchTxt` path `listStores` already implements — and both its
|
|
236
|
+
// caller and the public one fell back to `storeName_=`, a PREFIX match on
|
|
237
|
+
// the name alone. "Beyblade Arena" was unfindable by "arena".
|
|
238
|
+
return this.listStores(model, false, opts);
|
|
235
239
|
}
|
|
236
240
|
/**
|
|
237
241
|
* Cloud Functions: atomically increment `stats.totalProducts` counter.
|
|
@@ -2077,6 +2077,18 @@ const rawTesterChecklistItems = [
|
|
|
2077
2077
|
description: "/store/print-center is now a redirect, and it is deep-linked from the orders and products lists as ?type=order&ids=…&autoprint=1. Select two orders on /store/orders, use the print action, and confirm the Print Centre tab opens with those two orders loaded. Arriving at an EMPTY Print Centre is the failure — it looks like a normal empty state, which is why this case names the before.",
|
|
2078
2078
|
href: "/store/orders",
|
|
2079
2079
|
},
|
|
2080
|
+
{
|
|
2081
|
+
key: "search-box-promises-match-corpus",
|
|
2082
|
+
label: "Every dashboard search box matches what its placeholder promises",
|
|
2083
|
+
description: "BEFORE: four boxes promised a match the data could not make. /admin/prize-draws, /admin/deals and /admin/featured each read \"or seller\" while the product search index carries no store name at all, and /admin/stores read \"slugs, or owner names\" while the route ran a STARTS-WITH match on the store name alone. Each returned an empty list that reads as \"there are none\", not as \"this box cannot search that\". AFTER: type a seller's name into /admin/prize-draws — the placeholder no longer offers it. On /admin/stores type a word from the MIDDLE of a store's name (\"arena\", for Beyblade Arena) and a word from its description: both must now return it.",
|
|
2084
|
+
href: "/admin/stores",
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
key: "exact-search-boxes-commit-on-enter",
|
|
2088
|
+
label: "The two exact-match boxes say so and do not search while you type",
|
|
2089
|
+
description: "/admin/audit-log matches `actorUid` and /admin/notifications matches `userId`, both by EQUALITY — a partial id matches nothing. Both placeholders now say \"(exact)\". Type half a uid: the list must not churn per keystroke, and must not show \"no results\" until you press Enter. Each intermediate query would be a billed read that cannot match.",
|
|
2090
|
+
href: "/admin/audit-log",
|
|
2091
|
+
},
|
|
2080
2092
|
{
|
|
2081
2093
|
key: "consolidated-tabs-command-palette",
|
|
2082
2094
|
label: "The absorbed surfaces are still findable by name in search",
|