@mohasinac/appkit 4.56.0 → 4.58.0
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/.build-stamp.json +4 -4
- package/dist/_internal/server/features/beys/data.d.ts +44 -0
- package/dist/_internal/server/features/beys/data.js +107 -0
- package/dist/_internal/server/features/beys/og.d.ts +42 -0
- package/dist/_internal/server/features/beys/og.js +89 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +31 -0
- package/dist/_internal/server/features/checkout/actions.js +117 -5
- package/dist/_internal/server/features/orders/actions.d.ts +30 -0
- package/dist/_internal/server/features/orders/actions.js +103 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/parts/data.js +3 -2
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +139 -0
- package/dist/_internal/shared/shipping/quote.js +5 -6
- package/dist/client.d.ts +23 -0
- package/dist/client.js +3 -0
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +10 -0
- package/dist/constants/field-names.d.ts +46 -0
- package/dist/constants/field-names.js +46 -0
- package/dist/features/about/components/PublicProfileView.js +2 -1
- package/dist/features/account/components/UserOrdersView.js +25 -3
- package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
- package/dist/features/admin/components/AdminBeysView.js +178 -0
- package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
- package/dist/features/admin/components/AdminPartsView.js +160 -0
- package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
- package/dist/features/admin/components/OwnerQuickActions.js +83 -8
- package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
- package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
- package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
- package/dist/features/admin/schemas/firestore.d.ts +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +12 -1
- package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
- package/dist/features/beys/components/BeyDetailPageView.js +98 -0
- package/dist/features/beys/schemas/firestore.d.ts +417 -0
- package/dist/features/beys/schemas/firestore.js +65 -0
- package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/categories/components/BrandDetailPageView.js +4 -2
- package/dist/features/categories/components/BundleDetailView.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
- package/dist/features/categories/components/CategoryGrid.js +24 -2
- package/dist/features/categories/components/FranchiseDetailPageView.js +2 -1
- package/dist/features/events/components/EventCard.js +15 -2
- package/dist/features/faq/components/FAQPageContent.js +17 -4
- package/dist/features/faq/constants/help-guides.d.ts +36 -0
- package/dist/features/faq/constants/help-guides.js +64 -0
- package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
- package/dist/features/homepage/components/CustomCardsSection.js +67 -50
- package/dist/features/homepage/components/HeroCarousel.js +3 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/homepage/schemas/index.d.ts +10 -10
- package/dist/features/orders/components/EmiInstallmentsPanel.d.ts +19 -0
- package/dist/features/orders/components/EmiInstallmentsPanel.js +57 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +15 -0
- package/dist/features/parts/components/PartDetailPageView.js +5 -3
- package/dist/features/parts/components/PartsSearchBox.js +79 -4
- package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
- package/dist/features/parts/schemas/admin-edit.js +89 -0
- package/dist/features/parts/schemas/firestore.d.ts +2 -1
- package/dist/features/points/repository/points.repository.js +5 -5
- package/dist/features/points/schemas/firestore.d.ts +16 -0
- package/dist/features/points/schemas/firestore.js +17 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
- package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
- package/dist/features/products/components/ProductGrid.js +18 -2
- package/dist/features/products/schemas/firestore.d.ts +18 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +3 -2
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
- package/dist/features/seller/components/SellerProductShell.js +31 -3
- package/dist/features/stores/components/StoreHeader.js +3 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/next/routing/route-map.d.ts +17 -1
- package/dist/next/routing/route-map.js +39 -0
- package/dist/schemas/registry.d.ts +8 -8
- package/dist/seed/_helpers/category-models/chunk0.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk0.js +2648 -0
- package/dist/seed/_helpers/category-models/chunk1.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk1.js +2318 -0
- package/dist/seed/_helpers/category-models/chunk2.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk2.js +2560 -0
- package/dist/seed/_helpers/category-models/chunk3.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk3.js +2196 -0
- package/dist/seed/_helpers/category-models/chunk4.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk4.js +1732 -0
- package/dist/seed/_helpers/category-models.d.ts +9 -2
- package/dist/seed/_helpers/category-models.js +19 -1368
- package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
- package/dist/seed/_helpers/category-parts.js +39 -39
- package/dist/seed/bey-products-seed-data.d.ts +18 -0
- package/dist/seed/bey-products-seed-data.js +18011 -0
- package/dist/seed/beys/chunk0.d.ts +17 -0
- package/dist/seed/beys/chunk0.js +36733 -0
- package/dist/seed/beys/chunk1.d.ts +17 -0
- package/dist/seed/beys/chunk1.js +35728 -0
- package/dist/seed/beys/chunk2.d.ts +17 -0
- package/dist/seed/beys/chunk2.js +35857 -0
- package/dist/seed/beys/chunk3.d.ts +17 -0
- package/dist/seed/beys/chunk3.js +34476 -0
- package/dist/seed/beys-seed-data.d.ts +18 -0
- package/dist/seed/beys-seed-data.js +11 -0
- package/dist/seed/homepage-sections-seed-data.js +16 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seed/media-assets-seed-data.js +4 -4
- package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk0.js +7262 -0
- package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk1.js +7290 -0
- package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk10.js +7290 -0
- package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk11.js +7005 -0
- package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk12.js +7002 -0
- package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk13.js +7002 -0
- package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk14.js +7002 -0
- package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk15.js +7002 -0
- package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk16.js +7002 -0
- package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk17.js +7002 -0
- package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk18.js +4398 -0
- package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk2.js +7268 -0
- package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk3.js +7264 -0
- package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk4.js +7268 -0
- package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk5.js +7258 -0
- package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk6.js +7268 -0
- package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk7.js +7311 -0
- package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk8.js +7304 -0
- package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk9.js +7309 -0
- package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
- package/dist/seed/products-catalogue-seed-data.js +58 -86382
- package/dist/seed/seed-purpose.js +9 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +19 -0
- package/dist/styles.css +468 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PosterCard.d.ts +99 -0
- package/dist/ui/components/PosterCard.js +109 -0
- package/dist/ui/components/PosterCard.style.css +466 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +108 -1
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* `Record<Generation, Slot[]>` would be the hand-written enumeration of Root
|
|
30
30
|
* Cause #61 with a merge case it structurally cannot hold.
|
|
31
31
|
*/
|
|
32
|
+
import type { AdminEditableRecord } from "./admin-edit";
|
|
32
33
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
33
34
|
export declare const PART_GLOSSARY_COLLECTION: "partGlossary";
|
|
34
35
|
export declare const MODEL_COMPOSITION_COLLECTION: "modelComposition";
|
|
@@ -271,7 +272,7 @@ export interface PartWboLegality {
|
|
|
271
272
|
* source. A slot missing from that map yields no `role` rather than a guess.
|
|
272
273
|
*/
|
|
273
274
|
export type PartRole = "chip" | "ring" | "wheel" | "weight" | "base" | "blade" | "track" | "tip" | "gear" | "support";
|
|
274
|
-
export interface PartGlossaryDocument extends BaseDocument {
|
|
275
|
+
export interface PartGlossaryDocument extends BaseDocument, AdminEditableRecord {
|
|
275
276
|
/** `${slot}--${codeOrSlug}` — both halves, because a code is not unique. */
|
|
276
277
|
id: string;
|
|
277
278
|
/** Buyer-facing label: "Performance Tip", "Fusion Wheel". */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getAdminDb } from "../../../providers/db-firebase/index.js";
|
|
2
|
-
import { POINTS_LEDGER_COLLECTION, isPointsEntryWellFormed, } from "../schemas/firestore.js";
|
|
2
|
+
import { POINTS_FIELDS, POINTS_LEDGER_COLLECTION, isPointsEntryWellFormed, } from "../schemas/firestore.js";
|
|
3
3
|
import { USER_COLLECTION } from "../../auth/schemas/firestore.js";
|
|
4
4
|
/**
|
|
5
5
|
* The points ledger: append-only entries, and a balance that is their sum.
|
|
@@ -51,8 +51,8 @@ export class PointsRepository {
|
|
|
51
51
|
const db = getAdminDb();
|
|
52
52
|
const rows = await db
|
|
53
53
|
.collection(POINTS_LEDGER_COLLECTION)
|
|
54
|
-
.where(
|
|
55
|
-
.select(
|
|
54
|
+
.where(POINTS_FIELDS.USER_ID, "==", userId)
|
|
55
|
+
.select(POINTS_FIELDS.AMOUNT)
|
|
56
56
|
.get();
|
|
57
57
|
const total = rows.docs.reduce((sum, d) => sum + (Number(d.data().amount) || 0), 0);
|
|
58
58
|
return Math.round(total * 100) / 100;
|
|
@@ -145,8 +145,8 @@ export class PointsRepository {
|
|
|
145
145
|
const db = getAdminDb();
|
|
146
146
|
const rows = await db
|
|
147
147
|
.collection(POINTS_LEDGER_COLLECTION)
|
|
148
|
-
.where(
|
|
149
|
-
.orderBy(
|
|
148
|
+
.where(POINTS_FIELDS.USER_ID, "==", userId)
|
|
149
|
+
.orderBy(POINTS_FIELDS.CREATED_AT, "desc")
|
|
150
150
|
.limit(limit)
|
|
151
151
|
.get();
|
|
152
152
|
return rows.docs.map((d) => d.data());
|
|
@@ -81,6 +81,22 @@ export interface PointsLedgerEntry extends BaseDocument {
|
|
|
81
81
|
*/
|
|
82
82
|
export declare function isPointsEntryWellFormed(entry: Pick<PointsLedgerEntry, "kind" | "amount" | "adminReason">): boolean;
|
|
83
83
|
export declare const POINTS_LEDGER_COLLECTION = "pointsLedger";
|
|
84
|
+
/**
|
|
85
|
+
* Firestore field names for this collection.
|
|
86
|
+
*
|
|
87
|
+
* Declared rather than written as string literals at the query site, matching
|
|
88
|
+
* every other collection here: a typo in a raw `where("userID", ...)` is a
|
|
89
|
+
* query that returns nothing and raises nothing, which on a points balance
|
|
90
|
+
* reads as "you have no points" rather than as a bug.
|
|
91
|
+
*/
|
|
92
|
+
export declare const POINTS_FIELDS: {
|
|
93
|
+
readonly ID: "id";
|
|
94
|
+
readonly USER_ID: "userId";
|
|
95
|
+
readonly KIND: "kind";
|
|
96
|
+
readonly AMOUNT: "amount";
|
|
97
|
+
readonly ORDER_ID: "orderId";
|
|
98
|
+
readonly CREATED_AT: "createdAt";
|
|
99
|
+
};
|
|
84
100
|
/**
|
|
85
101
|
* 🛑 Points cannot pay for the whole order, and this is the cap that enforces
|
|
86
102
|
* it.
|
|
@@ -71,6 +71,22 @@ export function isPointsEntryWellFormed(entry) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
export const POINTS_LEDGER_COLLECTION = "pointsLedger";
|
|
74
|
+
/**
|
|
75
|
+
* Firestore field names for this collection.
|
|
76
|
+
*
|
|
77
|
+
* Declared rather than written as string literals at the query site, matching
|
|
78
|
+
* every other collection here: a typo in a raw `where("userID", ...)` is a
|
|
79
|
+
* query that returns nothing and raises nothing, which on a points balance
|
|
80
|
+
* reads as "you have no points" rather than as a bug.
|
|
81
|
+
*/
|
|
82
|
+
export const POINTS_FIELDS = {
|
|
83
|
+
ID: "id",
|
|
84
|
+
USER_ID: "userId",
|
|
85
|
+
KIND: "kind",
|
|
86
|
+
AMOUNT: "amount",
|
|
87
|
+
ORDER_ID: "orderId",
|
|
88
|
+
CREATED_AT: "createdAt",
|
|
89
|
+
};
|
|
74
90
|
/**
|
|
75
91
|
* 🛑 Points cannot pay for the whole order, and this is the cap that enforces
|
|
76
92
|
* it.
|
|
@@ -94,6 +110,7 @@ export function maxRedeemablePoints(orderTotal, balance) {
|
|
|
94
110
|
return 0;
|
|
95
111
|
if (!Number.isFinite(balance) || balance <= 0)
|
|
96
112
|
return 0;
|
|
113
|
+
// audit-money-units-ok: a percentage of an order total, not a paise/rupee conversion
|
|
97
114
|
const cap = Math.floor((orderTotal * POINTS_MAX_REDEMPTION_PERCENT) / 100);
|
|
98
115
|
return Math.max(0, Math.min(Math.floor(balance), cap));
|
|
99
116
|
}
|
|
@@ -19,6 +19,7 @@ import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
|
19
19
|
import { PreorderBadge } from "./PreorderCard.js";
|
|
20
20
|
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton.js";
|
|
21
21
|
import { CardStoreLine } from "../../products/components/CardStoreLine.js";
|
|
22
|
+
import { ProductOwnerQuickActions } from "../../products/components/ProductOwnerQuickActions.js";
|
|
22
23
|
const DEFAULT_LABELS = {
|
|
23
24
|
preOrderBadge: "Pre-order",
|
|
24
25
|
featuredBadge: "Featured",
|
|
@@ -69,11 +70,23 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
69
70
|
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
70
71
|
/* ── Full grid layout ── */
|
|
71
72
|
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
72
|
-
|
|
73
|
+
/*
|
|
74
|
+
* 🛑 Default, not opt-in. Nothing in either repo ever passed
|
|
75
|
+
* `renderOwnerActions`, so this toolbar was unreachable on every grid that
|
|
76
|
+
* mounts this card (Root Cause #8 — a slot shell rendered with zero render
|
|
77
|
+
* props). A card is resource-specific by construction, so it names its own
|
|
78
|
+
* resource rather than being threaded one; the prop stays as an override.
|
|
79
|
+
*/
|
|
80
|
+
const ownerActions = renderOwnerActions
|
|
81
|
+
? renderOwnerActions()
|
|
82
|
+
: product.id
|
|
83
|
+
? (_jsx(ProductOwnerQuickActions, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, recordLabel: product.title, placement: "card" }))
|
|
84
|
+
: null;
|
|
85
|
+
if (!ownerActions)
|
|
73
86
|
return card;
|
|
74
87
|
/*
|
|
75
88
|
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
76
89
|
* clears this card's own overlays, which all sit at `z-10`.
|
|
77
90
|
*/
|
|
78
|
-
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children:
|
|
91
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: ownerActions })] }));
|
|
79
92
|
}
|
|
@@ -14,6 +14,7 @@ import { BaseListingCard, Div, GatedPrice, PricesOnly, Row, Span, Stack, Text, T
|
|
|
14
14
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
15
15
|
import { computeBundleDiscount } from "../../../_internal/shared/features/categories/bundle-pricing.js";
|
|
16
16
|
import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy.js";
|
|
17
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
17
18
|
const DEFAULT_LABELS = {
|
|
18
19
|
bundleBadge: "Bundle",
|
|
19
20
|
inStockBadge: "In stock",
|
|
@@ -58,11 +59,23 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
58
59
|
? formatCurrency(price, getDefaultCurrency())
|
|
59
60
|
: "—" }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, getDefaultCurrency()) }))] }) }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.itemsLabel(memberCount) })] }), _jsx(Div, { className: CLS_VIEW_BTN, role: "button", tabIndex: 0, onClick: handleNavigate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ")
|
|
60
61
|
handleNavigate(); }, children: "View Bundle" })] })] }));
|
|
61
|
-
|
|
62
|
+
/*
|
|
63
|
+
* 🛑 Default, not opt-in. Nothing in either repo ever passed
|
|
64
|
+
* `renderOwnerActions`, so this toolbar was unreachable on every grid that
|
|
65
|
+
* mounts this card (Root Cause #8 — a slot shell rendered with zero render
|
|
66
|
+
* props). A card is resource-specific by construction, so it names its own
|
|
67
|
+
* resource rather than being threaded one; the prop stays as an override.
|
|
68
|
+
*/
|
|
69
|
+
const ownerActions = renderOwnerActions
|
|
70
|
+
? renderOwnerActions()
|
|
71
|
+
: bundle.id
|
|
72
|
+
? (_jsx(RecordOwnerQuickActions, { resource: "BUNDLE", recordId: String(bundle.id), recordLabel: String(bundle.name), placement: "card" }))
|
|
73
|
+
: null;
|
|
74
|
+
if (!ownerActions)
|
|
62
75
|
return card;
|
|
63
76
|
/*
|
|
64
77
|
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
65
78
|
* clears this card's own overlays, which all sit at `z-10`.
|
|
66
79
|
*/
|
|
67
|
-
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children:
|
|
80
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: ownerActions })] }));
|
|
68
81
|
}
|
|
@@ -11,6 +11,7 @@ const CLS_PRIZE_PILL = "inline-flex items-center rounded-full bg-fuchsia-600 px-
|
|
|
11
11
|
import { BaseListingCard, Button, Div, GatedPrice, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
12
12
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
13
13
|
import { CardStoreLine } from "./CardStoreLine.js";
|
|
14
|
+
import { ProductOwnerQuickActions } from "./ProductOwnerQuickActions.js";
|
|
14
15
|
const DEFAULT_LABELS = {
|
|
15
16
|
prizeDrawBadge: "Prize draw",
|
|
16
17
|
pendingBadge: "Reveal pending",
|
|
@@ -100,11 +101,23 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
100
101
|
}, className: selectable || isSelected
|
|
101
102
|
? "opacity-100"
|
|
102
103
|
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsxs(GatedPrice, { label: "Sign in to see the entry price", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }) }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
|
|
103
|
-
|
|
104
|
+
/*
|
|
105
|
+
* 🛑 Default, not opt-in. Nothing in either repo ever passed
|
|
106
|
+
* `renderOwnerActions`, so this toolbar was unreachable on every grid that
|
|
107
|
+
* mounts this card (Root Cause #8 — a slot shell rendered with zero render
|
|
108
|
+
* props). A card is resource-specific by construction, so it names its own
|
|
109
|
+
* resource rather than being threaded one; the prop stays as an override.
|
|
110
|
+
*/
|
|
111
|
+
const ownerActions = renderOwnerActions
|
|
112
|
+
? renderOwnerActions()
|
|
113
|
+
: product.id
|
|
114
|
+
? (_jsx(ProductOwnerQuickActions, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, recordLabel: product.title, placement: "card" }))
|
|
115
|
+
: null;
|
|
116
|
+
if (!ownerActions)
|
|
104
117
|
return card;
|
|
105
118
|
/*
|
|
106
119
|
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
107
120
|
* clears this card's own overlays, which all sit at `z-10`.
|
|
108
121
|
*/
|
|
109
|
-
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children:
|
|
122
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: ownerActions })] }));
|
|
110
123
|
}
|
|
@@ -17,6 +17,7 @@ import { PRODUCT_FEATURE_CARD_MAX_VISIBLE } from "../constants/product-features.
|
|
|
17
17
|
import { normalizeListingType } from "../utils/listing-type.js";
|
|
18
18
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
|
|
19
19
|
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton.js";
|
|
20
|
+
import { ProductOwnerQuickActions } from "./ProductOwnerQuickActions.js";
|
|
20
21
|
const __P = {
|
|
21
22
|
p3: "p-[var(--appkit-space-3)]",
|
|
22
23
|
};
|
|
@@ -125,7 +126,22 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
125
126
|
onAddToCart(product);
|
|
126
127
|
}, className: "justify-center text-[length:var(--appkit-text-xs)]", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
|
|
127
128
|
const wrapped = href && !selectionMode ? (_jsx(Link, { href: href, className: "block h-full", children: cardBody })) : (cardBody);
|
|
128
|
-
|
|
129
|
+
/*
|
|
130
|
+
* 🛑 Default, not opt-in — nothing in either repo ever passed
|
|
131
|
+
* `renderOwnerActions`, so this toolbar was unreachable on every product
|
|
132
|
+
* grid in the app despite the prop, the docstring and the positioning all
|
|
133
|
+
* being present (Root Cause #8).
|
|
134
|
+
*
|
|
135
|
+
* `ProductOwnerQuickActions`, not the generic wrapper: a product is the one
|
|
136
|
+
* collection a SELLER can also own, so its endpoints live under
|
|
137
|
+
* `SELLER_ENDPOINTS` and its clone has a bespoke redirect to the copy's
|
|
138
|
+
* editor. `storeId` is the store SLUG, which is what `SessionUser.storeId`
|
|
139
|
+
* holds — the component renders nothing for anyone who owns neither.
|
|
140
|
+
*/
|
|
141
|
+
const ownerActions = renderOwnerActions ? (renderOwnerActions()) : product.id ? (_jsx(ProductOwnerQuickActions, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, recordLabel: product.title, placement: "card" })) : null;
|
|
142
|
+
// Selection is still suppressed: a bulk-select gesture and a per-record edit
|
|
143
|
+
// are different intents, and offering both at once invites the wrong one.
|
|
144
|
+
if (!ownerActions || selectionMode || isSelected)
|
|
129
145
|
return wrapped;
|
|
130
146
|
/*
|
|
131
147
|
* 🛑 OUTSIDE the anchor, as a sibling — never inside `cardBody`.
|
|
@@ -144,7 +160,7 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
144
160
|
*
|
|
145
161
|
* `z-20` beats the in-card overlays, which all sit at `z-10`.
|
|
146
162
|
*/
|
|
147
|
-
return (_jsxs(Div, { className: "relative h-full", children: [wrapped, _jsx(Div, { className: "absolute right-2 top-14 z-20", children:
|
|
163
|
+
return (_jsxs(Div, { className: "relative h-full", children: [wrapped, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: ownerActions })] }));
|
|
148
164
|
}
|
|
149
165
|
function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
|
|
150
166
|
const discount = product.originalPrice && product.originalPrice > product.price
|
|
@@ -379,6 +379,24 @@ export interface ProductDocument extends BaseDocument {
|
|
|
379
379
|
* defects welded into the TITLE.
|
|
380
380
|
*/
|
|
381
381
|
unitLabel?: string;
|
|
382
|
+
/**
|
|
383
|
+
* This listing is a complete BEY, and this is which one.
|
|
384
|
+
*
|
|
385
|
+
* The `beys` document id, which is also its slug and also the URL
|
|
386
|
+
* (`/beys/{beyId}`) with no re-derivation — the same property that made
|
|
387
|
+
* `partGlossaryId` a single id rather than a pair.
|
|
388
|
+
*
|
|
389
|
+
* 🛑 THE EXACT COUNTERPART OF `partGlossaryId`, and the pair is what makes
|
|
390
|
+
* the seller's "single" choice meaningful: a single item is either one named
|
|
391
|
+
* bey or one named part, and those are the only two things in the catalogue
|
|
392
|
+
* that have an identity to point at. A lot has neither, which is why it
|
|
393
|
+
* files under a non-leaf CATEGORY instead (see `isLot`).
|
|
394
|
+
*
|
|
395
|
+
* 🛑 Never set together with `partGlossaryId`. A listing is a whole bey or a
|
|
396
|
+
* component, not both — and the two link to different reference pages, so a
|
|
397
|
+
* row carrying both would render two contradictory "what is this" sections.
|
|
398
|
+
*/
|
|
399
|
+
beyId?: string;
|
|
382
400
|
/**
|
|
383
401
|
* This listing is a COMPONENT, and this is which one.
|
|
384
402
|
*
|
|
@@ -2,10 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Link from "next/link.js";
|
|
3
3
|
import { reviewRepository } from "../../../repositories/index.js";
|
|
4
4
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
5
|
-
import { Container, Div, Grid, Heading, Main, Nav, Section, Span, Text } from "../../../ui/index.js";
|
|
5
|
+
import { Container, Div, Grid, Heading, Main, Nav, Row, Section, Span, Text } from "../../../ui/index.js";
|
|
6
6
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
7
|
import { ReviewDetailShell } from "./ReviewDetailShell.js";
|
|
8
8
|
import { ReviewCard } from "./ReviewsList.js";
|
|
9
|
+
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
9
10
|
export async function ReviewDetailPageView({ id }) {
|
|
10
11
|
// The review IS this page's subject — a read failure must surface rather than
|
|
11
12
|
// become the same `null` a removed review produces, which the "Review not
|
|
@@ -42,5 +43,5 @@ export async function ReviewDetailPageView({ id }) {
|
|
|
42
43
|
]);
|
|
43
44
|
const sameProductReviews = sameProductDocs.filter((r) => r.id !== review.id).slice(0, 6);
|
|
44
45
|
const sameStoreReviews = sameStoreDocs.filter((r) => r.id !== review.id).slice(0, 6);
|
|
45
|
-
return (_jsxs(Main, { children: [_jsx(Div, { paddingY: "y-xs-tall", className: "border-b border-neutral-100 bg-neutral-50", padding: "x-md", children: _jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "xs", color: "faint", className: "mx-auto max-w-3xl", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary transition-colors", children: "Home" }), _jsx(Span, { children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "hover:text-primary transition-colors", children: "Reviews" }), _jsx(Span, { children: "/" }), _jsx(Span, { className: "text-neutral-700 truncate max-w-[200px]", children: review.title ?? review.id })] }) }), _jsx(ReviewDetailShell, { review: review, storeHref: storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null }), sameProductReviews.length > 0 && (_jsx(Section, { padding: "y-lg", border: "subtle", className: "border-t", children: _jsxs(Container, { size: "sm", children: [_jsxs(Heading, { level: 2, className: "mb-4", size: "lg", weight: "semibold", children: ["More reviews for ", review.productTitle ?? "this product"] }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: sameProductReviews.map((r) => (_jsx(ReviewCard, { review: r, context: "listing" }, r.id))) })] }) })), sameStoreReviews.length > 0 && (_jsx(Section, { padding: "y-lg", border: "subtle", className: "border-t", children: _jsxs(Container, { size: "sm", children: [_jsxs(Heading, { level: 2, className: "mb-4", size: "lg", weight: "semibold", children: ["More reviews for ", review.storeName ?? "this store"] }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: sameStoreReviews.map((r) => (_jsx(ReviewCard, { review: r, context: "store" }, r.id))) })] }) }))] }));
|
|
46
|
+
return (_jsxs(Main, { children: [_jsx(Div, { paddingY: "y-xs-tall", className: "border-b border-neutral-100 bg-neutral-50", padding: "x-md", children: _jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "xs", color: "faint", className: "mx-auto max-w-3xl", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary transition-colors", children: "Home" }), _jsx(Span, { children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "hover:text-primary transition-colors", children: "Reviews" }), _jsx(Span, { children: "/" }), _jsx(Span, { className: "text-neutral-700 truncate max-w-[200px]", children: review.title ?? review.id })] }) }), _jsx(Row, { justify: "end", className: "mb-2", children: _jsx(OwnerQuickActions, { resource: "REVIEW", placement: "detail", ownerId: review.userId, recordLabel: review.title ?? review.id, editHref: String(ROUTES.USER.REVIEW_EDIT(review.id)) }) }), _jsx(ReviewDetailShell, { review: review, storeHref: storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null }), sameProductReviews.length > 0 && (_jsx(Section, { padding: "y-lg", border: "subtle", className: "border-t", children: _jsxs(Container, { size: "sm", children: [_jsxs(Heading, { level: 2, className: "mb-4", size: "lg", weight: "semibold", children: ["More reviews for ", review.productTitle ?? "this product"] }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: sameProductReviews.map((r) => (_jsx(ReviewCard, { review: r, context: "listing" }, r.id))) })] }) })), sameStoreReviews.length > 0 && (_jsx(Section, { padding: "y-lg", border: "subtle", className: "border-t", children: _jsxs(Container, { size: "sm", children: [_jsxs(Heading, { level: 2, className: "mb-4", size: "lg", weight: "semibold", children: ["More reviews for ", review.storeName ?? "this store"] }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: sameStoreReviews.map((r) => (_jsx(ReviewCard, { review: r, context: "store" }, r.id))) })] }) }))] }));
|
|
46
47
|
}
|
|
@@ -22,5 +22,14 @@ export interface CategoryInlineSelectProps {
|
|
|
22
22
|
* `MAX_CATEGORY_ASSIGNMENTS`, which is what the repository refuses above.
|
|
23
23
|
*/
|
|
24
24
|
maxAssignments?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Scope the rows to LEAVES (a single item) or to COLLECTIONS (a lot).
|
|
27
|
+
*
|
|
28
|
+
* 🛑 `undefined` means UNSCOPED, which is what every existing call site
|
|
29
|
+
* gets — this cannot default, because a caller that does not know whether it
|
|
30
|
+
* is a lot must not be silently told it is a single. See `leafFilterFor`
|
|
31
|
+
* for why the server makes this load-bearing rather than cosmetic.
|
|
32
|
+
*/
|
|
33
|
+
leavesOnly?: boolean;
|
|
25
34
|
}
|
|
26
|
-
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, extraValues, onExtraValuesChange, maxAssignments, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
35
|
+
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, extraValues, onExtraValuesChange, maxAssignments, leavesOnly, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
3
4
|
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
4
5
|
import { CategoryQuickCreateForm } from "../../admin/components/CategoryQuickCreateForm.js";
|
|
5
6
|
import { apiClient } from "../../../http/index.js";
|
|
@@ -7,14 +8,48 @@ import { ADMIN_ENDPOINTS, CATEGORY_ENDPOINTS } from "../../../constants/api-endp
|
|
|
7
8
|
import { Stack } from "../../../ui/components/Layout.js";
|
|
8
9
|
import { Text } from "../../../ui/components/Typography.js";
|
|
9
10
|
import { MAX_CATEGORY_ASSIGNMENTS } from "../../products/repository/category-assignment.js";
|
|
11
|
+
/**
|
|
12
|
+
* ── 🛑 LOT vs SINGLE SCOPES THE ROWS, AND THE SERVER ALREADY REFUSES THE
|
|
13
|
+
* WRONG ONE ───────────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* `assertAssignmentShape` has enforced both directions since 2026-10-14
|
|
16
|
+
* (`ENFORCE_LEAF_FILING = true`):
|
|
17
|
+
*
|
|
18
|
+
* a LOT at a LEAF -> "A lot holds several different items, so it cannot
|
|
19
|
+
* be filed under X, which is a single-item category."
|
|
20
|
+
* a SINGLE at a BRANCH -> "X has subcategories, so it describes a group
|
|
21
|
+
* rather than one item."
|
|
22
|
+
*
|
|
23
|
+
* Until now the picker offered EVERY category to both, so roughly half of all
|
|
24
|
+
* choices walked the seller into a 400 they could not act on. Scoping the rows
|
|
25
|
+
* is therefore not a convenience — it is the client half of a rule the server
|
|
26
|
+
* already holds.
|
|
27
|
+
*
|
|
28
|
+
* 🛑 `isLeaf` NEEDS NO ROUTE CHANGE. `/api/admin/categories` forwards its
|
|
29
|
+
* `filters` string straight into `categoriesRepository.list`, and the public
|
|
30
|
+
* `/api/categories` whitelists `isLeaf` in `SAFE_CATEGORY_FILTER_FIELDS`. So
|
|
31
|
+
* this is a query-string addition, not new endpoint surface — which is also
|
|
32
|
+
* why it can ship without a build to verify it.
|
|
33
|
+
*
|
|
34
|
+
* `undefined` means UNSCOPED, which is what every existing caller gets. A
|
|
35
|
+
* caller that does not know whether it is a lot must not be silently told it
|
|
36
|
+
* is a single.
|
|
37
|
+
*/
|
|
38
|
+
function leafFilterFor(leavesOnly) {
|
|
39
|
+
if (leavesOnly === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
return leavesOnly ? "isLeaf==true" : "isLeaf==false";
|
|
42
|
+
}
|
|
10
43
|
// Admin: paginated + server-side search via /api/admin/categories (admin/mod only).
|
|
11
44
|
// Response via successResponse({ data: items, total }) → apiClient unwraps to { data, total, ... }.
|
|
12
|
-
async function loadAdminCategoryOptions(query, page) {
|
|
45
|
+
async function loadAdminCategoryOptions(query, page, leavesOnly) {
|
|
46
|
+
const leafFilter = leafFilterFor(leavesOnly);
|
|
13
47
|
const params = new URLSearchParams({
|
|
14
48
|
q: query,
|
|
15
49
|
page: String(page),
|
|
16
50
|
pageSize: "20",
|
|
17
51
|
flat: "true",
|
|
52
|
+
...(leafFilter ? { filters: leafFilter } : {}),
|
|
18
53
|
});
|
|
19
54
|
const res = await apiClient.get(`${ADMIN_ENDPOINTS.CATEGORIES}?${params}`);
|
|
20
55
|
const items = (res.data ?? []).map((c) => ({
|
|
@@ -26,8 +61,13 @@ async function loadAdminCategoryOptions(query, page) {
|
|
|
26
61
|
}
|
|
27
62
|
// Seller: fetch all flat categories from the public unauthenticated endpoint,
|
|
28
63
|
// filter client-side by query. Public endpoint returns { success, data: CategoryItem[] }.
|
|
29
|
-
async function loadPublicCategoryOptions(query, _page) {
|
|
30
|
-
const
|
|
64
|
+
async function loadPublicCategoryOptions(query, _page, leavesOnly) {
|
|
65
|
+
const leafFilter = leafFilterFor(leavesOnly);
|
|
66
|
+
const params = new URLSearchParams({
|
|
67
|
+
flat: "true",
|
|
68
|
+
pageSize: "200",
|
|
69
|
+
...(leafFilter ? { filters: leafFilter } : {}),
|
|
70
|
+
});
|
|
31
71
|
const res = await fetch(CATEGORY_ENDPOINTS.FILTERED(params.toString()), { credentials: "include" });
|
|
32
72
|
const json = await res.json().catch(() => ({}));
|
|
33
73
|
const all = (json.data ?? []).map((c) => ({
|
|
@@ -38,8 +78,16 @@ async function loadPublicCategoryOptions(query, _page) {
|
|
|
38
78
|
const items = q ? all.filter((c) => c.label.toLowerCase().includes(q)) : all;
|
|
39
79
|
return { items, hasMore: false, nextPage: 2 };
|
|
40
80
|
}
|
|
41
|
-
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, extraValues, onExtraValuesChange, maxAssignments = MAX_CATEGORY_ASSIGNMENTS, }) {
|
|
42
|
-
|
|
81
|
+
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, extraValues, onExtraValuesChange, maxAssignments = MAX_CATEGORY_ASSIGNMENTS, leavesOnly, }) {
|
|
82
|
+
/*
|
|
83
|
+
* 🛑 MEMOISED, because `PaginatedSelect` consumes `loadOptions` inside an
|
|
84
|
+
* effect keyed on query/page. A fresh function identity every render
|
|
85
|
+
* re-triggers that effect, and the existing loaders are module-scope
|
|
86
|
+
* functions precisely to avoid it — binding the scope here would reintroduce
|
|
87
|
+
* the loop if it were done inline.
|
|
88
|
+
*/
|
|
89
|
+
const base = allowCreate ? loadAdminCategoryOptions : loadPublicCategoryOptions;
|
|
90
|
+
const loadOptions = React.useCallback((query, page) => base(query, page, leavesOnly), [base, leavesOnly]);
|
|
43
91
|
const primary = (_jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "category", drawerTitle: "Create Category", renderCreateForm: allowCreate
|
|
44
92
|
? ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
45
93
|
: undefined }));
|
|
@@ -19,6 +19,28 @@ import type { ListingType } from "../../products/types/index";
|
|
|
19
19
|
export type ProductListingMode = ListingType | "bundle";
|
|
20
20
|
export interface SellerProductDraft {
|
|
21
21
|
title?: string;
|
|
22
|
+
/**
|
|
23
|
+
* LOT or SINGLE — and this had a field and a server rule but no control.
|
|
24
|
+
*
|
|
25
|
+
* 🛑 `ProductDocument.isLot` has existed since G3a and
|
|
26
|
+
* `ProductRepository.deriveTaxonomy` ENFORCES it at line 488
|
|
27
|
+
* (`assertAssignmentShape(resolved, input.isLot === true, …)`): a lot must
|
|
28
|
+
* file under a non-leaf category, a single may file anywhere. The form never
|
|
29
|
+
* set it, so every listing reached that check as a single — and a seller
|
|
30
|
+
* with a mixed pile had no way to say otherwise.
|
|
31
|
+
*
|
|
32
|
+
* The reason the rule exists is a claim about the ITEMS, not about counting:
|
|
33
|
+
* filing "12 assorted Burst beys" under `category-dragoon-g` asserts all
|
|
34
|
+
* twelve are Dragoon G.
|
|
35
|
+
*/
|
|
36
|
+
isLot?: boolean;
|
|
37
|
+
/** How many items the lot holds. Optional even when `isLot` — a seller who
|
|
38
|
+
* has not counted the pile yet must still be able to save a draft. */
|
|
39
|
+
lotSize?: number;
|
|
40
|
+
/** The named bey this single IS, when the seller picked one. */
|
|
41
|
+
beyId?: string;
|
|
42
|
+
/** The named PART this single is, when it is a component rather than a bey. */
|
|
43
|
+
partGlossaryId?: string;
|
|
22
44
|
slug?: string;
|
|
23
45
|
description?: string;
|
|
24
46
|
category?: string;
|
|
@@ -170,6 +192,23 @@ export interface SellerProductShellProps {
|
|
|
170
192
|
renderCategorySelector?: (props: {
|
|
171
193
|
value: string;
|
|
172
194
|
onChange: (v: string) => void;
|
|
195
|
+
/**
|
|
196
|
+
* Whether the listing is a LOT, so the selector can offer the right rows.
|
|
197
|
+
*
|
|
198
|
+
* 🛑 OPTIONAL, like `extraValues` beside it and for the same measured
|
|
199
|
+
* reason: `QuickProductForm` is a second consumer of this slot and making
|
|
200
|
+
* a new prop required broke it immediately last time (Root Cause #20, a
|
|
201
|
+
* public prop change without its consumers — which the comment below
|
|
202
|
+
* already warns about). A selector that ignores this keeps today's
|
|
203
|
+
* behaviour exactly.
|
|
204
|
+
*
|
|
205
|
+
* 🛑 The server has enforced the split since 2026-10-14
|
|
206
|
+
* (`ENFORCE_LEAF_FILING = true`): a lot at a leaf and a single at a branch
|
|
207
|
+
* are both refused. So a selector that offers the wrong rows is not a
|
|
208
|
+
* cosmetic issue — it walks the seller into a 400 they cannot act on,
|
|
209
|
+
* which is what every seller hit before this prop existed.
|
|
210
|
+
*/
|
|
211
|
+
isLot?: boolean;
|
|
173
212
|
/**
|
|
174
213
|
* 🛑 OPTIONAL, and that is the whole point. `QuickProductForm` is a second
|
|
175
214
|
* consumer of this slot and making these required broke it immediately —
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation.js";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -46,6 +46,21 @@ const sellerProductSchema = z.object({
|
|
|
46
46
|
title: z.string().min(1, "Product title is required").max(200),
|
|
47
47
|
description: z.string().max(10000).optional().or(z.literal("")),
|
|
48
48
|
price: z.number().min(0).optional(),
|
|
49
|
+
/*
|
|
50
|
+
* LOT vs SINGLE. 🛑 Declared HERE as well as on the interface, for the exact
|
|
51
|
+
* reason the `categoryIds` note below gives: this schema is
|
|
52
|
+
* `.passthrough()`, so an undeclared key survives at runtime and stays
|
|
53
|
+
* UNTYPED — the render site could not read `values.isLot` at all.
|
|
54
|
+
*
|
|
55
|
+
* No `.default(false)`, matching `src/validation/request-schemas.ts`: absent
|
|
56
|
+
* means single, and a default would write a field the seller never touched.
|
|
57
|
+
*/
|
|
58
|
+
isLot: z.boolean().optional(),
|
|
59
|
+
/* Bounds mirror the request schema (2–999). A "lot of 1" is a single. */
|
|
60
|
+
lotSize: z.number().int().min(2).max(999).optional(),
|
|
61
|
+
/* The single-item identity, one or the other and never both. */
|
|
62
|
+
beyId: z.string().optional(),
|
|
63
|
+
partGlossaryId: z.string().optional(),
|
|
49
64
|
category: z.string().optional(),
|
|
50
65
|
/**
|
|
51
66
|
* I2 — the assignment list, DEFAULT FIRST. `category` mirrors `[0]`.
|
|
@@ -116,9 +131,22 @@ function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelect
|
|
|
116
131
|
condition: CONDITION_OPTIONS.find((o) => o.value === (values.condition ?? "new"))?.label ?? "",
|
|
117
132
|
siteName: "LetItRip",
|
|
118
133
|
};
|
|
119
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(
|
|
134
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: "What is in this listing?" }), _jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsx(Toggle, { checked: values.isLot === true, onChange: (next) => onChange(next
|
|
135
|
+
? /*
|
|
136
|
+
* Switching TO a lot clears the single-item identity. A
|
|
137
|
+
* stale `beyId` would keep the listing pointing at one
|
|
138
|
+
* named bey while claiming to be a mixed pile — the
|
|
139
|
+
* contradiction the server rule exists to prevent, smuggled
|
|
140
|
+
* past it by a field the seller can no longer see.
|
|
141
|
+
*/
|
|
142
|
+
{ isLot: true, beyId: undefined, partGlossaryId: undefined }
|
|
143
|
+
: /* And back again drops the count, for the same reason. */
|
|
144
|
+
{ isLot: false, lotSize: undefined }), label: values.isLot ? "A lot of several items" : "A single item", size: "sm" }), values.isLot && (_jsx(FormField, { name: "lotSize", label: "How many?", type: "number", value: values.lotSize != null ? String(values.lotSize) : "", onChange: (v) => onChange({ lotSize: v ? Number(v) : undefined }), placeholder: "optional" }))] }), _jsx(Text, { size: "xs", color: "muted", children: values.isLot
|
|
145
|
+
? "A lot files under a category rather than one model, because a category is a collection — naming one model would claim every item in the pile is that model."
|
|
146
|
+
: "A single item can be filed against the exact bey or part it is, so buyers searching that name find it." })] }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
120
147
|
value: values.category ?? "",
|
|
121
148
|
onChange: (v) => onChange({ category: v }),
|
|
149
|
+
isLot: values.isLot === true,
|
|
122
150
|
/*
|
|
123
151
|
* 🛑 `categoryIds[0]` is the DEFAULT assignment and is already
|
|
124
152
|
* rendered by the primary picker above, so the extras list is
|
|
@@ -596,7 +624,7 @@ productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCat
|
|
|
596
624
|
label: "Basic",
|
|
597
625
|
required: true,
|
|
598
626
|
quick: true,
|
|
599
|
-
fields: ["title", "description", "category", "brand", "condition", "tags", "barcodeId"],
|
|
627
|
+
fields: ["title", "isLot", "lotSize", "description", "category", "brand", "condition", "tags", "barcodeId"],
|
|
600
628
|
render: ({ values, onChange }) => (_jsx(StepBasic, { values: values, onChange: onChange, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })),
|
|
601
629
|
schema: sectionSchemas.basic,
|
|
602
630
|
},
|
|
@@ -4,6 +4,8 @@ import { MediaImage } from "../../media/MediaImage.js";
|
|
|
4
4
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
5
5
|
import { ShareButton } from "../../products/components/ShareButton.js";
|
|
6
6
|
import { SellerTrustBadge } from "../../scams/components/SellerTrustBadge.js";
|
|
7
|
+
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
8
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
9
|
const __O = {
|
|
8
10
|
hidden: "overflow-hidden",
|
|
9
11
|
};
|
|
@@ -12,7 +14,7 @@ const CLS_STARS = "inline-flex items-center gap-[var(--appkit-space-1)] text-war
|
|
|
12
14
|
const CLS_FOLLOW_BTN = "rounded-lg border border-warning px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-warning hover:bg-warning-surface transition-colors";
|
|
13
15
|
const CLS_WARN_BANNER = "mt-3 rounded-lg bg-warning-surface dark:bg-warning-surface border border-warning dark:border-warning px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-warning dark:text-warning";
|
|
14
16
|
export function StoreHeader({ store, labels = {}, onFollow, className = "", trust, }) {
|
|
15
|
-
return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] })), trust && _jsx(SellerTrustBadge, { trust: trust })] }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
|
|
17
|
+
return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] })), trust && _jsx(SellerTrustBadge, { trust: trust }), _jsx(OwnerQuickActions, { resource: "STORE", placement: "detail", editHref: String(ROUTES.ADMIN.STORE_DETAIL(store.storeSlug)), recordLabel: store.storeName })] }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
|
|
16
18
|
labels.vacationMode ??
|
|
17
19
|
"Store is on vacation mode" }))] })] }));
|
|
18
20
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2990,6 +2990,12 @@ export { taxCodesSeedData } from "./seed/index";
|
|
|
2990
2990
|
export { PART_GLOSSARY_COLLECTION, MODEL_COMPOSITION_COLLECTION, PART_SLOT_VALUES, PART_SLOT_LABEL, ALL_PART_SLOTS, LISTING_TYPE_HAS_COMPOSITION, partSlotFromSegment, partGlossaryId, } from "./features/parts/schemas/firestore";
|
|
2991
2991
|
export type { PartGlossaryDocument, ModelCompositionDocument, ModelSlot, ModelSlotMerge, PartSlot, } from "./features/parts/schemas/firestore";
|
|
2992
2992
|
export { partGlossarySeedData } from "./seed/index";
|
|
2993
|
+
export { BEYS_COLLECTION, BEY_PRODUCTS_COLLECTION, beyId, } from "./features/beys/schemas/firestore";
|
|
2994
|
+
export type { BeyDocument, BeyProductDocument, BeyCompositionEntry, BeyRatings, BeyDerivationCoverage, BeyProductAssortmentEntry, } from "./features/beys/schemas/firestore";
|
|
2995
|
+
export { beysSeedData, BEY_SEED_COUNT } from "./seed/index";
|
|
2996
|
+
export { BeyDetailPageView } from "./features/beys/components/BeyDetailPageView";
|
|
2997
|
+
export type { BeyDetailPageViewProps, BeyListingRow } from "./features/beys/components/BeyDetailPageView";
|
|
2998
|
+
export { beyProductsSeedData, BEY_PRODUCT_SEED_COUNT } from "./seed/index";
|
|
2993
2999
|
export { modelCompositionSeedData } from "./seed/index";
|
|
2994
3000
|
export { ModelCompositionPanel } from "./features/parts/components/ModelCompositionPanel";
|
|
2995
3001
|
export { PartsIndexPageView, PartClassificationPageView, } from "./features/parts/components/PartsIndexPageView";
|
|
@@ -3067,6 +3073,8 @@ export { LISTING_TYPE_CAPABILITIES, capabilityFor, canAddToCart, canBid, support
|
|
|
3067
3073
|
export type { ListingTypeCapability } from "./_internal/shared/listing-types/capabilities";
|
|
3068
3074
|
export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
|
|
3069
3075
|
export { OwnerQuickActions } from "./features/admin/components/OwnerQuickActions";
|
|
3076
|
+
export { RecordOwnerQuickActions } from "./features/admin/components/RecordOwnerQuickActions";
|
|
3077
|
+
export type { RecordOwnerQuickActionsProps, RecordQuickActionResource } from "./features/admin/components/RecordOwnerQuickActions";
|
|
3070
3078
|
export type { OwnerQuickActionsProps, OwnerQuickActionsResource } from "./features/admin/components/OwnerQuickActions";
|
|
3071
3079
|
export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
|
|
3072
3080
|
export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
|
package/dist/index.js
CHANGED
|
@@ -5403,6 +5403,18 @@ export { taxCodesSeedData } from "./seed/index.js";
|
|
|
5403
5403
|
// E4/D12 — the collection constants and types.
|
|
5404
5404
|
export { PART_GLOSSARY_COLLECTION, MODEL_COMPOSITION_COLLECTION, PART_SLOT_VALUES, PART_SLOT_LABEL, ALL_PART_SLOTS, LISTING_TYPE_HAS_COMPOSITION, partSlotFromSegment, partGlossaryId, } from "./features/parts/schemas/firestore.js";
|
|
5405
5405
|
export { partGlossarySeedData } from "./seed/index.js";
|
|
5406
|
+
/*
|
|
5407
|
+
* 🛑 Exported from the MAIN barrel, not just `seed/index.ts`. `seed-cli.mjs`
|
|
5408
|
+
* destructures its seed data from the BARE `@mohasinac/appkit` package, which
|
|
5409
|
+
* resolves through THIS file — so an export present only on the inner barrel is
|
|
5410
|
+
* invisible to the loader and the collection reports "no seed data, skipping"
|
|
5411
|
+
* with exit 0. That is Root Cause #90 exactly, and it is how `mediaAssets`
|
|
5412
|
+
* read "504 seed / 0 in db" while all 504 objects sat in the bucket.
|
|
5413
|
+
*/
|
|
5414
|
+
export { BEYS_COLLECTION, BEY_PRODUCTS_COLLECTION, beyId, } from "./features/beys/schemas/firestore.js";
|
|
5415
|
+
export { beysSeedData, BEY_SEED_COUNT } from "./seed/index.js";
|
|
5416
|
+
export { BeyDetailPageView } from "./features/beys/components/BeyDetailPageView.js";
|
|
5417
|
+
export { beyProductsSeedData, BEY_PRODUCT_SEED_COUNT } from "./seed/index.js";
|
|
5406
5418
|
export { modelCompositionSeedData } from "./seed/index.js";
|
|
5407
5419
|
// D12 — the public part surfaces.
|
|
5408
5420
|
export { ModelCompositionPanel } from "./features/parts/components/ModelCompositionPanel.js";
|
|
@@ -5494,6 +5506,7 @@ export { LISTING_TYPE_CAPABILITIES, capabilityFor, canAddToCart, canBid, support
|
|
|
5494
5506
|
export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry.js";
|
|
5495
5507
|
// Pencil / clone / bin for whoever owns the record, on a PUBLIC surface.
|
|
5496
5508
|
export { OwnerQuickActions } from "./features/admin/components/OwnerQuickActions.js";
|
|
5509
|
+
export { RecordOwnerQuickActions } from "./features/admin/components/RecordOwnerQuickActions.js";
|
|
5497
5510
|
// Checkout rule registry â€" pure TS, safe in both client and server bundles.
|
|
5498
5511
|
export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules/index.js";
|
|
5499
5512
|
// Media upload limits â€" pure constants, safe in both client and server bundles.
|