@mohasinac/appkit 4.53.1 → 4.54.0-next.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 +5 -5
- package/dist/_internal/client/theme/ThemeProvider.js +15 -1
- package/dist/_internal/server/features/auth/capabilities.js +7 -5
- package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
- package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
- package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
- package/dist/_internal/server/features/price-index/market.d.ts +57 -0
- package/dist/_internal/server/features/price-index/market.js +120 -0
- package/dist/_internal/server/features/products/list-public.d.ts +12 -0
- package/dist/_internal/server/features/products/list-public.js +10 -1
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
- package/dist/_internal/server/features/products/listing-tab-counts.js +184 -10
- package/dist/_internal/server/features/search/index-build/sources.js +43 -2
- package/dist/_internal/server/features/stores/tabs.d.ts +7 -2
- package/dist/_internal/server/features/stores/tabs.js +5 -3
- package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
- package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +128 -0
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
- package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
- package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
- package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
- package/dist/_internal/shared/listing-types/tiers.js +150 -0
- package/dist/_internal/shared/search/match.js +28 -2
- package/dist/cli/index.js +0 -2
- package/dist/client.d.ts +21 -2
- package/dist/client.js +23 -4
- package/dist/constants/api-endpoints.d.ts +28 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/account/components/UserOrdersView.js +26 -7
- package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
- package/dist/features/addresses/utils/postal-autofill.js +54 -0
- package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
- package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
- package/dist/features/admin/components/OwnerQuickActions.js +142 -0
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +8 -7
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
- package/dist/features/auth/permissions/constants.d.ts +29 -2
- package/dist/features/auth/permissions/constants.js +68 -1
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
- package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
- package/dist/features/cart/components/CartDrawer.js +3 -2
- package/dist/features/cart/components/CartGroupLineRow.js +2 -1
- package/dist/features/cart/hooks/useAddToCart.js +10 -5
- package/dist/features/cart/utils/pending-ops.d.ts +11 -1
- package/dist/features/cart/utils/pending-ops.js +14 -2
- package/dist/features/categories/api/[id]/price-guidance/route.js +1 -24
- package/dist/features/categories/components/BrandDetailPageView.js +15 -6
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/BrandDetailTabs.js +18 -3
- package/dist/features/categories/components/BundleCollage.js +3 -2
- package/dist/features/categories/components/BundleItemsPicker.js +3 -4
- package/dist/features/categories/components/CategoryDetailPageView.js +19 -8
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +29 -7
- package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
- package/dist/features/categories/components/CategoryGrid.js +9 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
- package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +10 -1
- package/dist/features/events/components/EventCard.d.ts +11 -1
- package/dist/features/events/components/EventCard.js +9 -2
- package/dist/features/layout/AppLayoutShell.js +2 -1
- package/dist/features/layout/BottomActions.d.ts +1 -0
- package/dist/features/layout/BottomActions.js +330 -0
- package/dist/features/layout/BottomChrome.d.ts +45 -0
- package/dist/features/layout/BottomChrome.js +5 -17
- package/dist/features/layout/SideActionDock.js +36 -13
- package/dist/features/layout/index.d.ts +2 -7
- package/dist/features/layout/index.js +7 -7
- package/dist/features/layout/useIsDesktop.d.ts +36 -0
- package/dist/features/layout/useIsDesktop.js +48 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +10 -1
- package/dist/features/media/messages/en.json +67 -67
- package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
- package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
- package/dist/features/orders/components/index.d.ts +2 -1
- package/dist/features/orders/components/index.js +1 -1
- package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
- package/dist/features/orders/utils/order-announcement-message.js +16 -0
- package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
- package/dist/features/orders/utils/payment-proof-message.js +17 -0
- package/dist/features/parts/schemas/firestore.d.ts +227 -0
- package/dist/features/parts/schemas/firestore.js +6 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +8 -7
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +21 -0
- package/dist/features/price-index/components/PriceIndexToolView.js +230 -27
- package/dist/features/price-index/lookup-target.d.ts +48 -0
- package/dist/features/price-index/lookup-target.js +107 -0
- package/dist/features/products/api/[id]/route.js +8 -1
- package/dist/features/products/api/route.js +16 -4
- package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
- package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
- package/dist/features/products/components/CompareOverlay.d.ts +2 -1
- package/dist/features/products/components/CompareOverlay.js +2 -1
- package/dist/features/products/components/GroupSettingsPanel.js +2 -1
- package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
- package/dist/features/products/components/InteractiveProductCard.js +16 -4
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
- package/dist/features/products/components/ProductDetailPageView.js +34 -4
- package/dist/features/products/components/ProductGrid.d.ts +14 -1
- package/dist/features/products/components/ProductGrid.js +22 -5
- package/dist/features/products/components/ProductsIndexListing.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -0
- package/dist/features/products/repository/products.repository.js +5 -1
- package/dist/features/products/utils/group-member-image.d.ts +9 -2
- package/dist/features/products/utils/group-member-image.js +2 -1
- package/dist/features/reviews/api/[id]/route.js +83 -23
- package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
- package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
- package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
- package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
- package/dist/features/reviews/components/index.d.ts +4 -0
- package/dist/features/reviews/components/index.js +2 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +49 -3
- package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
- package/dist/features/seller/components/SellerProductsView.js +3 -2
- package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
- package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
- package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/index.d.ts +0 -2
- package/dist/features/stores/components/index.js +0 -1
- package/dist/features/stores/schemas/index.d.ts +1 -1
- package/dist/features/stores/schemas/index.js +10 -1
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +114 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +415 -0
- package/dist/index.d.ts +8 -27
- package/dist/index.js +14 -35
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +9 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
- package/dist/providers/shipping-shiprocket/index.js +353 -0
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/security/csp.d.ts +16 -1
- package/dist/security/csp.js +5 -3
- package/dist/seed/_helpers/category-forest.js +27 -0
- package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
- package/dist/seed/_helpers/category-parts.d.ts +1 -1
- package/dist/seed/_helpers/category-parts.js +13412 -3020
- package/dist/seed/categories-seed-data.js +48 -6
- package/dist/seed/homepage-sections-seed-data.js +41 -28
- package/dist/seed/model-composition-seed-data.js +6 -8
- package/dist/seed/part-glossary/chunk0.d.ts +9 -0
- package/dist/seed/part-glossary/chunk0.js +35044 -0
- package/dist/seed/part-glossary/chunk1.d.ts +9 -0
- package/dist/seed/part-glossary/chunk1.js +39955 -0
- package/dist/seed/part-glossary/chunk2.d.ts +9 -0
- package/dist/seed/part-glossary/chunk2.js +50335 -0
- package/dist/seed/part-glossary/chunk3.d.ts +9 -0
- package/dist/seed/part-glossary/chunk3.js +52386 -0
- package/dist/seed/part-glossary/chunk4.d.ts +9 -0
- package/dist/seed/part-glossary/chunk4.js +50549 -0
- package/dist/seed/part-glossary-seed-data.d.ts +1 -1
- package/dist/seed/part-glossary-seed-data.js +23 -47483
- package/dist/seed/products-beyblade-x-seed-data.js +36 -1
- package/dist/server.d.ts +6 -8
- package/dist/server.js +16 -20
- package/dist/styles.css +516 -358
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/index.d.ts +3 -1
- package/dist/tokens/themes/index.js +11 -1
- package/dist/tokens/themes/technical-dark.d.ts +30 -0
- package/dist/tokens/themes/technical-dark.js +103 -0
- package/dist/tokens/themes/technical-light.d.ts +22 -0
- package/dist/tokens/themes/technical-light.js +89 -0
- package/dist/tokens/tokens.css +155 -0
- package/dist/ui/components/GatedPrice.js +30 -3
- package/dist/ui/components/GatedPrice.style.css +22 -19
- package/dist/ui/components/QuickCreateModal.style.css +78 -78
- package/dist/ui/forms/FormShell.style.css +260 -260
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +133 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
- package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
- package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
- package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
- package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
- package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
- package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
- package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
- package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/components/index.js +0 -2
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
- package/dist/features/whatsapp-bot/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/index.js +0 -3
- package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
- package/dist/features/whatsapp-bot/manifest.js +0 -13
- package/dist/features/whatsapp-bot/messages/en.json +0 -7
- package/dist/features/whatsapp-bot/server.d.ts +0 -7
- package/dist/features/whatsapp-bot/server.js +0 -7
- package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
- package/dist/features/whatsapp-bot/types/index.js +0 -1
|
@@ -79,6 +79,11 @@ export const ACCOUNT_ENDPOINTS = {
|
|
|
79
79
|
HISTORY_ITEM: (productId) => `/api/user/history/${encodeURIComponent(productId)}`,
|
|
80
80
|
HISTORY_MERGE: "/api/user/history/merge",
|
|
81
81
|
REVIEWS: "/api/user/reviews",
|
|
82
|
+
/**
|
|
83
|
+
* "Have I bought this, and did I review it?" for one product — backs the
|
|
84
|
+
* purchase strip on a product page. Per-viewer, never cached.
|
|
85
|
+
*/
|
|
86
|
+
PURCHASE_CONTEXT: (productId) => `/api/user/purchases/${encodeURIComponent(productId)}`,
|
|
82
87
|
BIDS: "/api/user/bids",
|
|
83
88
|
BID_BY_ID: (id) => `/api/user/bids/${id}`,
|
|
84
89
|
EXPORT: "/api/user/export",
|
|
@@ -348,6 +353,13 @@ export const TOOL_ENDPOINTS = {
|
|
|
348
353
|
* is the multi-get `searchPriceIndex` otherwise pays for every lookup.
|
|
349
354
|
*/
|
|
350
355
|
PRICE_INDEX_BANDS: (ids) => `/api/tools/price-index/bands?ids=${ids.map(encodeURIComponent).join(",")}`,
|
|
356
|
+
/**
|
|
357
|
+
* The competitor-crawl market band for the same leaves — SIGNED-IN ONLY, and
|
|
358
|
+
* therefore a separate request from `PRICE_INDEX_BANDS` rather than more
|
|
359
|
+
* fields on it. That route is public and edge-cached; this one must not be,
|
|
360
|
+
* or one signed-in lookup populates an entry every guest reads.
|
|
361
|
+
*/
|
|
362
|
+
PRICE_INDEX_MARKET: (ids) => `/api/tools/price-index/market?ids=${ids.map(encodeURIComponent).join(",")}`,
|
|
351
363
|
};
|
|
352
364
|
/**
|
|
353
365
|
* The sticker-sheet kiss-cut tracer. Admin-only, every route behind
|
|
@@ -585,6 +597,8 @@ export const PROMOTION_ENDPOINTS = {
|
|
|
585
597
|
export const REVIEW_ENDPOINTS = {
|
|
586
598
|
LIST: "/api/reviews",
|
|
587
599
|
FEATURED: "/api/reviews?featured=true",
|
|
600
|
+
/** GET / PATCH / DELETE one review. The PATCH is the buyer's own edit. */
|
|
601
|
+
BY_ID: (id) => `/api/reviews/${encodeURIComponent(id)}`,
|
|
588
602
|
};
|
|
589
603
|
// ---------------------------------------------------------------------------
|
|
590
604
|
// Search
|
|
@@ -11,7 +11,23 @@ import { OrderCard } from "../../orders/components/OrdersList.js";
|
|
|
11
11
|
import { useOrderScope } from "../../orders/components/OrderScopeTabs.js";
|
|
12
12
|
import { useSearchParams } from "next/navigation.js";
|
|
13
13
|
const CANCELLABLE_STATUSES = new Set(["pending", "confirmed", "processing"]);
|
|
14
|
-
|
|
14
|
+
/*
|
|
15
|
+
* 🛑 Every PRE-DELIVERY status, not just "shipped".
|
|
16
|
+
*
|
|
17
|
+
* It was `["shipped"]` alone, so a buyer whose order was pending, confirmed
|
|
18
|
+
* or processing had no "where is it" link at all — which is precisely when
|
|
19
|
+
* they most want one. The timeline on the track page reads correctly for all
|
|
20
|
+
* of these: it shows the stages reached and the stages still ahead.
|
|
21
|
+
*
|
|
22
|
+
* Delivered, cancelled, refunded and returned are excluded because tracking a
|
|
23
|
+
* finished order is the order detail page's job, not a courier's.
|
|
24
|
+
*/
|
|
25
|
+
const TRACKABLE_STATUSES = new Set([
|
|
26
|
+
"pending",
|
|
27
|
+
"confirmed",
|
|
28
|
+
"processing",
|
|
29
|
+
"shipped",
|
|
30
|
+
]);
|
|
15
31
|
const SORT_OPTIONS = [
|
|
16
32
|
{ value: sortBy("createdAt", "DESC"), label: "Newest first" },
|
|
17
33
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest first" },
|
|
@@ -77,12 +93,15 @@ export function UserOrdersView({ onOrderClick }) {
|
|
|
77
93
|
portal: "user",
|
|
78
94
|
title: "My Orders",
|
|
79
95
|
search: {
|
|
80
|
-
placeholder: "Search by order id…",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
placeholder: "Search by item or order id…",
|
|
97
|
+
/*
|
|
98
|
+
* The route now matches product titles as well as the order id, so the
|
|
99
|
+
* placeholder can promise it. It said "order id" because that was all
|
|
100
|
+
* the route could do, with a comment calling that "the honest promise
|
|
101
|
+
* until the route can match more" — this is the promise catching up,
|
|
102
|
+
* not a widened claim.
|
|
103
|
+
*/
|
|
104
|
+
fields: ["id", "productTitle"],
|
|
86
105
|
},
|
|
87
106
|
emptyLabel: emptyMessage,
|
|
88
107
|
filterKeys: ["status", "orderType"],
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface CityState {
|
|
2
|
+
city: string;
|
|
3
|
+
state: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* What the last autofill wrote, so a later one can tell its own output apart
|
|
7
|
+
* from a human's typing. A field is absent when the helper declined to write
|
|
8
|
+
* it — which makes it the human's, and untouchable.
|
|
9
|
+
*/
|
|
10
|
+
export type PostalAutofillMemo = Partial<CityState>;
|
|
11
|
+
/**
|
|
12
|
+
* Merge `resolved` into `prev`, returning the new values and an updated memo.
|
|
13
|
+
*
|
|
14
|
+
* ── 🛑 THE BUG THIS EXISTS TO FIX ─────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* Both address forms used `prev.city.trim() ? prev.city : city` — fill only
|
|
17
|
+
* an EMPTY field. That intent is right and is preserved here: correcting an
|
|
18
|
+
* address someone deliberately typed is a bug they cannot report, because
|
|
19
|
+
* they watch their own input vanish with no idea what did it.
|
|
20
|
+
*
|
|
21
|
+
* But "empty" cannot tell WHO filled the field. After the first lookup the
|
|
22
|
+
* city is non-empty — written by the autofill itself — so every subsequent
|
|
23
|
+
* lookup is refused. Enter 413001, get Solapur/Maharashtra; correct the PIN
|
|
24
|
+
* to 500005 (Hyderabad) and the form still shows Solapur. The PIN and the
|
|
25
|
+
* city now disagree, the form validates, and the parcel ships to the wrong
|
|
26
|
+
* city — worse than never autofilling at all.
|
|
27
|
+
*
|
|
28
|
+
* ── The rule ──────────────────────────────────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* A field is ours to overwrite when it is EMPTY, or when it still holds
|
|
31
|
+
* exactly what we last wrote into it. The moment a human edits it, it stops
|
|
32
|
+
* matching the memo and becomes untouchable again — the original guarantee,
|
|
33
|
+
* now actually enforceable rather than approximated by "is it blank".
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyPostalAutofill<T extends CityState>(prev: T, resolved: Partial<CityState>, memo: PostalAutofillMemo): {
|
|
36
|
+
next: T;
|
|
37
|
+
memo: PostalAutofillMemo;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: A postal lookup must fill the city/state it knows WITHOUT overwriting
|
|
3
|
+
* what the person typed — and the obvious test for that ("is the field
|
|
4
|
+
* empty?") silently breaks every lookup after the first.
|
|
5
|
+
* WHAT: `applyPostalAutofill` — merge a resolved city/state into form values,
|
|
6
|
+
* overwriting only values this helper itself wrote earlier.
|
|
7
|
+
*
|
|
8
|
+
* EXPORTS: applyPostalAutofill, type PostalAutofillMemo
|
|
9
|
+
*
|
|
10
|
+
* @tag domain:addresses
|
|
11
|
+
* @tag layer:utils
|
|
12
|
+
* @tag pattern:pure
|
|
13
|
+
* @tag sideEffects:none
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Merge `resolved` into `prev`, returning the new values and an updated memo.
|
|
17
|
+
*
|
|
18
|
+
* ── 🛑 THE BUG THIS EXISTS TO FIX ─────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* Both address forms used `prev.city.trim() ? prev.city : city` — fill only
|
|
21
|
+
* an EMPTY field. That intent is right and is preserved here: correcting an
|
|
22
|
+
* address someone deliberately typed is a bug they cannot report, because
|
|
23
|
+
* they watch their own input vanish with no idea what did it.
|
|
24
|
+
*
|
|
25
|
+
* But "empty" cannot tell WHO filled the field. After the first lookup the
|
|
26
|
+
* city is non-empty — written by the autofill itself — so every subsequent
|
|
27
|
+
* lookup is refused. Enter 413001, get Solapur/Maharashtra; correct the PIN
|
|
28
|
+
* to 500005 (Hyderabad) and the form still shows Solapur. The PIN and the
|
|
29
|
+
* city now disagree, the form validates, and the parcel ships to the wrong
|
|
30
|
+
* city — worse than never autofilling at all.
|
|
31
|
+
*
|
|
32
|
+
* ── The rule ──────────────────────────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* A field is ours to overwrite when it is EMPTY, or when it still holds
|
|
35
|
+
* exactly what we last wrote into it. The moment a human edits it, it stops
|
|
36
|
+
* matching the memo and becomes untouchable again — the original guarantee,
|
|
37
|
+
* now actually enforceable rather than approximated by "is it blank".
|
|
38
|
+
*/
|
|
39
|
+
export function applyPostalAutofill(prev, resolved, memo) {
|
|
40
|
+
const nextMemo = {};
|
|
41
|
+
const merge = (field) => {
|
|
42
|
+
const current = prev[field];
|
|
43
|
+
const incoming = resolved[field];
|
|
44
|
+
const ours = current.trim() === "" || current === memo[field];
|
|
45
|
+
if (!ours || !incoming)
|
|
46
|
+
return current; // human's value, or nothing to say
|
|
47
|
+
nextMemo[field] = incoming;
|
|
48
|
+
return incoming;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
next: { ...prev, city: merge("city"), state: merge("state") },
|
|
52
|
+
memo: nextMemo,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -6,11 +6,20 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
6
6
|
import { Checkbox, Details, Div, SideDrawer, Span, Stack, Summary, useToast } from "../../../ui/index.js";
|
|
7
7
|
import { apiClient } from "../../../http/index.js";
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
9
|
+
import { ALL_STORE_CAPABILITIES, DEFAULT_STORE_CAPABILITIES, } from "../../auth/permissions/constants.js";
|
|
9
10
|
import { adminStoreUpdateSchema } from "../schemas/admin-editor-forms.js";
|
|
10
11
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
|
|
11
12
|
import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell.js";
|
|
12
13
|
import { SectionForm, useSectionFormNav, buildSectionsFromSchema } from "../../shell/index.js";
|
|
13
|
-
|
|
14
|
+
/*
|
|
15
|
+
* 🛑 Imported, not restated. This value is the `??` fallback on the load path
|
|
16
|
+
* below, so whatever it holds is what gets SAVED BACK for a store whose
|
|
17
|
+
* `capabilities` field is absent. A local copy that drifted from
|
|
18
|
+
* `DEFAULT_STORE_CAPABILITIES` would silently rewrite entitlements on the next
|
|
19
|
+
* unrelated edit — Root Cause #38's re-save shape, with two owners of one
|
|
20
|
+
* default as the cause.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_CAPABILITIES = [...DEFAULT_STORE_CAPABILITIES];
|
|
14
23
|
/** Add or remove one capability, returning the new list. */
|
|
15
24
|
function toggleCapability(current, key) {
|
|
16
25
|
const next = new Set(current);
|
|
@@ -20,41 +29,71 @@ function toggleCapability(current, key) {
|
|
|
20
29
|
next.add(key);
|
|
21
30
|
return Array.from(next);
|
|
22
31
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{ key: "early_access_features", label: "Early Access Features" },
|
|
53
|
-
{ key: "advanced_analytics", label: "Advanced Analytics" },
|
|
54
|
-
{ key: "whatsapp_catalog_sync", label: "WhatsApp Catalog Sync" },
|
|
55
|
-
],
|
|
32
|
+
/**
|
|
33
|
+
* The three capability groups, named once.
|
|
34
|
+
*
|
|
35
|
+
* A literal repeated per capability is how a group silently splits in two the
|
|
36
|
+
* day someone types "Trust and Visibility" in one of nineteen entries — the
|
|
37
|
+
* `CAPABILITY_GROUPS` derivation below filters on equality, so the typo'd entry
|
|
38
|
+
* would land in no rendered group at all and that capability would vanish from
|
|
39
|
+
* the admin UI without any error.
|
|
40
|
+
*/
|
|
41
|
+
const CAP_GROUP = {
|
|
42
|
+
LISTINGS: "Listings",
|
|
43
|
+
TRUST: "Trust & Visibility",
|
|
44
|
+
PLATFORM: "Platform",
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Every capability's group and label.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 `Record<StoreCapability, …>`, so a capability added to the union MUST be
|
|
50
|
+
* given a home here or this file does not compile. The groups used to be three
|
|
51
|
+
* hand-written arrays, which meant a new capability could be granted in code,
|
|
52
|
+
* checked by a server gate, and remain **unreachable from the admin UI** — the
|
|
53
|
+
* only place it can be turned on. That is the whole drift class in one
|
|
54
|
+
* sentence, and it had already happened: `advanced_listing_types` is the
|
|
55
|
+
* nineteenth member and the arrays listed eighteen.
|
|
56
|
+
*/
|
|
57
|
+
const CAPABILITY_META = {
|
|
58
|
+
advanced_listing_types: {
|
|
59
|
+
group: CAP_GROUP.LISTINGS,
|
|
60
|
+
label: "Advanced Listing Types (pre-orders, prize draws, classifieds, digital codes, live, art, stickers)",
|
|
56
61
|
},
|
|
62
|
+
// Legacy: no longer checked anywhere — `auction` is a basic type now.
|
|
63
|
+
host_auctions: { group: CAP_GROUP.LISTINGS, label: "Host Auctions (legacy — no longer required)" },
|
|
64
|
+
// Legacy per-type grant, still honoured so existing stores keep access.
|
|
65
|
+
host_preorders: { group: CAP_GROUP.LISTINGS, label: "Host Pre-orders (legacy grant)" },
|
|
66
|
+
create_categories: { group: CAP_GROUP.LISTINGS, label: "Request New Categories" },
|
|
67
|
+
suggest_brands: { group: CAP_GROUP.LISTINGS, label: "Suggest Brands" },
|
|
68
|
+
create_coupons: { group: CAP_GROUP.LISTINGS, label: "Create Coupons" },
|
|
69
|
+
bulk_listing_import: { group: CAP_GROUP.LISTINGS, label: "Bulk Listing Import" },
|
|
70
|
+
extended_return_window: { group: CAP_GROUP.LISTINGS, label: "Extended Return Window" },
|
|
71
|
+
verified_seller: { group: CAP_GROUP.TRUST, label: "Verified Seller Badge" },
|
|
72
|
+
featured_placement: { group: CAP_GROUP.TRUST, label: "Featured Placement" },
|
|
73
|
+
promotional_banner: { group: CAP_GROUP.TRUST, label: "Promotional Banner" },
|
|
74
|
+
priority_support: { group: CAP_GROUP.TRUST, label: "Priority Support" },
|
|
75
|
+
multiple_stores: { group: CAP_GROUP.PLATFORM, label: "Multiple Stores" },
|
|
76
|
+
custom_store_slug: { group: CAP_GROUP.PLATFORM, label: "Custom Store Slug" },
|
|
77
|
+
api_access: { group: CAP_GROUP.PLATFORM, label: "API Access" },
|
|
78
|
+
lower_commission_rate: { group: CAP_GROUP.PLATFORM, label: "Lower Commission Rate" },
|
|
79
|
+
early_access_features: { group: CAP_GROUP.PLATFORM, label: "Early Access Features" },
|
|
80
|
+
advanced_analytics: { group: CAP_GROUP.PLATFORM, label: "Advanced Analytics" },
|
|
81
|
+
whatsapp_catalog_sync: { group: CAP_GROUP.PLATFORM, label: "WhatsApp Catalog Sync" },
|
|
82
|
+
};
|
|
83
|
+
const CAPABILITY_GROUP_ORDER = [
|
|
84
|
+
CAP_GROUP.LISTINGS,
|
|
85
|
+
CAP_GROUP.TRUST,
|
|
86
|
+
CAP_GROUP.PLATFORM,
|
|
57
87
|
];
|
|
88
|
+
/**
|
|
89
|
+
* Derived, so every capability appears in exactly one group by construction.
|
|
90
|
+
* Keeps the `{ label, caps: [{ key, label }] }` shape the renderer below
|
|
91
|
+
* already expects.
|
|
92
|
+
*/
|
|
93
|
+
const CAPABILITY_GROUPS = CAPABILITY_GROUP_ORDER.map((group) => ({
|
|
94
|
+
label: group,
|
|
95
|
+
caps: ALL_STORE_CAPABILITIES.filter((key) => CAPABILITY_META[key].group === group).map((key) => ({ key, label: CAPABILITY_META[key].label })),
|
|
96
|
+
}));
|
|
58
97
|
const STATUS_OPTIONS = [
|
|
59
98
|
{ label: "Active", value: "active" },
|
|
60
99
|
{ label: "Pending", value: "pending" },
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Which registry bucket supplies the labels and the confirm copy. */
|
|
2
|
+
export type OwnerQuickActionsResource = "PRODUCT" | "CATEGORY" | "BRAND" | "BUNDLE" | "GROUP" | "SUBLISTING" | "EVENT" | "BLOG" | "REVIEW";
|
|
3
|
+
export interface OwnerQuickActionsProps {
|
|
4
|
+
resource: OwnerQuickActionsResource;
|
|
5
|
+
/** Where the pencil goes — that record's dashboard editor. */
|
|
6
|
+
editHref: string;
|
|
7
|
+
/** Called after the confirm is accepted. Must do the DELETE itself. */
|
|
8
|
+
onDelete?: () => void | Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Admin-only clone. Omit entirely for resources with no duplicate endpoint —
|
|
11
|
+
* categories, bundles, carousel slides and homepage sections all have
|
|
12
|
+
* derived or positional state that a copy would carry wrongly, so the
|
|
13
|
+
* absence is deliberate rather than unfinished.
|
|
14
|
+
*/
|
|
15
|
+
onClone?: () => void | Promise<void>;
|
|
16
|
+
/** Auth UID of the owner, when the record has one. Admins pass regardless. */
|
|
17
|
+
ownerId?: string;
|
|
18
|
+
/** Store SLUG for store-owned records — `SessionUser.storeId` is the slug. */
|
|
19
|
+
storeId?: string;
|
|
20
|
+
/** Names the thing in the confirm dialog: "Delete Dranzer S?" */
|
|
21
|
+
recordLabel?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `card` sits in a hero corner and uses a scrim so it reads on photography;
|
|
24
|
+
* `detail` sits in a header row beside the title.
|
|
25
|
+
*/
|
|
26
|
+
placement?: "card" | "detail";
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Pencil / clone / bin, on a PUBLIC surface, for whoever owns the record.
|
|
5
|
+
*
|
|
6
|
+
* ── What this is for ──────────────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* An admin or seller spots a problem on the page a visitor sees — a wrong
|
|
9
|
+
* price, a typo, a bad photo — and today has to navigate to the dashboard,
|
|
10
|
+
* find the list, search for the row and open it. This is the two-click path to
|
|
11
|
+
* the editor from where the problem was noticed.
|
|
12
|
+
*
|
|
13
|
+
* ── Why it is a new component and not a slot on each card ─────────────────
|
|
14
|
+
*
|
|
15
|
+
* 🛑 Measured before building: ZERO public detail views render any owner-only
|
|
16
|
+
* control, and the only precedent anywhere is `CouponCard`, which has had
|
|
17
|
+
* `onEdit` / `onToggleActive` / `onDelete` props the whole time and whose two
|
|
18
|
+
* public consumers pass NONE of them. So there was no pattern to follow and no
|
|
19
|
+
* second implementation to reconcile — one component, used by every surface,
|
|
20
|
+
* is what keeps the visibility rule and the confirm copy in one place.
|
|
21
|
+
*
|
|
22
|
+
* ── The visibility rule, and why it has three states ──────────────────────
|
|
23
|
+
*
|
|
24
|
+
* 🛑 Public pages are static or ISR (`products/[slug]` is `revalidate = 3600`
|
|
25
|
+
* and reads no session), so `SessionProvider` mounts `initialUser={null}` and
|
|
26
|
+
* `loading` starts TRUE on every hard load — for signed-in users too. A
|
|
27
|
+
* two-state `!user → hide` gate therefore renders nothing, then pops the
|
|
28
|
+
* toolbar in a few hundred milliseconds later, on every single page load.
|
|
29
|
+
*
|
|
30
|
+
* So: render NOTHING while `isAuthResolving`. Appearing late is invisible;
|
|
31
|
+
* appearing with a flash is a bug people report. Same reasoning as
|
|
32
|
+
* `useCanSeePrices`, which exists for the mirror-image problem.
|
|
33
|
+
*
|
|
34
|
+
* ── Who sees what ─────────────────────────────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* pencil + bin the record's owner, or any admin
|
|
37
|
+
* clone admins ONLY
|
|
38
|
+
*
|
|
39
|
+
* 🛑 Clone is gated on `useIsAdmin()`, NOT on the same check as the other two.
|
|
40
|
+
* `useIsOwner` returns `true` for admins AND for the owner, so reusing it
|
|
41
|
+
* would hand every seller a button that is meant to be a staff tool.
|
|
42
|
+
*
|
|
43
|
+
* @tag domain:admin
|
|
44
|
+
* @tag layer:components
|
|
45
|
+
* @tag access:client
|
|
46
|
+
*/
|
|
47
|
+
import { useCallback, useState } from "react";
|
|
48
|
+
import { useIsAdmin, useIsOwner } from "../../auth/hooks/useRBAC.js";
|
|
49
|
+
import { useOptionalSession } from "../../../react/contexts/SessionContext.js";
|
|
50
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
51
|
+
import { ConfirmDeleteModal } from "../../../ui/components/ConfirmDeleteModal.js";
|
|
52
|
+
import { Icon } from "../../../ui/components/Icon.js";
|
|
53
|
+
import { IconButton } from "../../../ui/components/IconButton.js";
|
|
54
|
+
import { Row, useToast } from "../../../ui/index.js";
|
|
55
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
56
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
57
|
+
export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement = "card", className = "", }) {
|
|
58
|
+
const session = useOptionalSession();
|
|
59
|
+
const isOwnerOrAdmin = useIsOwner(ownerId);
|
|
60
|
+
const isAdmin = useIsAdmin();
|
|
61
|
+
const { showToast } = useToast();
|
|
62
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
63
|
+
const [deleting, setDeleting] = useState(false);
|
|
64
|
+
const bucket = ACTIONS[resource];
|
|
65
|
+
const handleConfirmedDelete = useCallback(async () => {
|
|
66
|
+
if (!onDelete)
|
|
67
|
+
return;
|
|
68
|
+
setDeleting(true);
|
|
69
|
+
try {
|
|
70
|
+
await onDelete();
|
|
71
|
+
setConfirmOpen(false);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const normalized = normalizeError(err);
|
|
75
|
+
/*
|
|
76
|
+
* 🛑 A failed delete MUST say so. Without this the dialog simply closed
|
|
77
|
+
* its spinner and the record was still there — and because the caller
|
|
78
|
+
* navigates away on success, "nothing happened" is indistinguishable
|
|
79
|
+
* from "it worked but the page did not move". This is a destructive
|
|
80
|
+
* action on a public page; silence is the worst available answer.
|
|
81
|
+
*/
|
|
82
|
+
showToast(toUserMessage(normalized.code ?? "UNKNOWN"), "error");
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
// Always clears, so a failed delete leaves a usable dialog rather than a
|
|
86
|
+
// permanently-spinning button the user cannot dismiss or retry.
|
|
87
|
+
setDeleting(false);
|
|
88
|
+
}
|
|
89
|
+
}, [onDelete, showToast]);
|
|
90
|
+
/*
|
|
91
|
+
* 🛑 Nothing at all until auth resolves — see the header. `loading` starts
|
|
92
|
+
* true on every hard load of a static page, so this is the common path on
|
|
93
|
+
* first paint, not an edge case.
|
|
94
|
+
*/
|
|
95
|
+
if (!session || session.loading)
|
|
96
|
+
return null;
|
|
97
|
+
/*
|
|
98
|
+
* A store-owned record is also "mine" when I own that store.
|
|
99
|
+
* `SessionUser.storeId` holds the store SLUG and `StoreDocument.storeId` is
|
|
100
|
+
* the slug too, while `ownerId` is the Auth UID — two different id spaces,
|
|
101
|
+
* so both have to be checked rather than one standing in for the other.
|
|
102
|
+
*/
|
|
103
|
+
const ownsStore = Boolean(storeId && session.user?.storeId === storeId);
|
|
104
|
+
/*
|
|
105
|
+
* 🛑 `isAdmin` is its OWN term, not folded into `useIsOwner`.
|
|
106
|
+
*
|
|
107
|
+
* `useIsOwner` early-returns `false` when `resourceOwnerId` is falsy — BEFORE
|
|
108
|
+
* it checks admin — so for a record with no owner at all it denies admins
|
|
109
|
+
* too. Categories, brands, franchises, carousel slides and homepage sections
|
|
110
|
+
* have no `ownerId`, and those are precisely the collections an admin needs
|
|
111
|
+
* this toolbar on. Relying on `useIsOwner` alone would have hidden it from
|
|
112
|
+
* the only person who can edit them.
|
|
113
|
+
*/
|
|
114
|
+
if (!isAdmin && !isOwnerOrAdmin && !ownsStore)
|
|
115
|
+
return null;
|
|
116
|
+
const editDef = bucket.edit;
|
|
117
|
+
const deleteDef = bucket.delete;
|
|
118
|
+
const confirmation = deleteDef?.confirmation;
|
|
119
|
+
const showClone = Boolean(onClone) && isAdmin;
|
|
120
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Row, { gap: "xs", align: "center", className: className,
|
|
121
|
+
/*
|
|
122
|
+
* Stops the click reaching an ancestor `<Link>` or the card's own
|
|
123
|
+
* `onClick`. Without it, pressing Edit on a card both navigates to the
|
|
124
|
+
* editor AND opens the public detail page behind it.
|
|
125
|
+
*/
|
|
126
|
+
onClick: (e) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
}, children: [_jsx(IconButton, { "aria-label": editDef?.ariaLabel ?? editDef?.label ?? "Edit", size: "touch", variant: placement === "card" ? "scrim" : "ghost", icon: _jsx(Icon, { name: "edit" }), onClick: () => {
|
|
129
|
+
/*
|
|
130
|
+
* `window.location`, not `next/link`. This component is rendered
|
|
131
|
+
* inside cards whose whole body is already an anchor, and nesting
|
|
132
|
+
* an `<a>` in an `<a>` is invalid HTML that browsers resolve by
|
|
133
|
+
* closing the outer one early — which breaks the card's own
|
|
134
|
+
* navigation, not just this button's.
|
|
135
|
+
*/
|
|
136
|
+
if (typeof window !== "undefined")
|
|
137
|
+
window.location.assign(editHref);
|
|
138
|
+
} }), showClone && (_jsx(IconButton, { "aria-label": (bucket.duplicate?.ariaLabel ?? bucket.duplicate?.label) ||
|
|
139
|
+
"Duplicate", size: "touch", variant: placement === "card" ? "scrim" : "ghost", icon: _jsx(Icon, { name: "duplicate" }), onClick: () => void onClone?.() })), onDelete && (_jsx(IconButton, { "aria-label": deleteDef?.ariaLabel ?? deleteDef?.label ?? "Delete", size: "touch", variant: placement === "card" ? "scrim" : "danger", icon: _jsx(Icon, { name: "delete" }), onClick: () => setConfirmOpen(true) }))] }), onDelete && (_jsx(ConfirmDeleteModal, { isOpen: confirmOpen, onClose: () => setConfirmOpen(false), onConfirm: handleConfirmedDelete, isDeleting: deleting, title: confirmation?.title ?? "Delete this record?", message: recordLabel && confirmation
|
|
140
|
+
? `${confirmation.body}\n\nDeleting: ${recordLabel}`
|
|
141
|
+
: confirmation?.body, confirmText: confirmation?.confirmLabel ?? deleteDef?.label ?? "Delete", variant: "danger" }))] }));
|
|
142
|
+
}
|
|
@@ -12,7 +12,7 @@ export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
12
12
|
isVerified: z.ZodOptional<z.ZodBoolean>;
|
|
13
13
|
isFeatured: z.ZodOptional<z.ZodBoolean>;
|
|
14
14
|
suspensionReason: z.ZodOptional<z.ZodString>;
|
|
15
|
-
capabilities: z.ZodOptional<z.ZodArray<z.
|
|
15
|
+
capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
|
|
16
16
|
adminNotes: z.ZodOptional<z.ZodString>;
|
|
17
17
|
}, "strip", z.ZodTypeAny, {
|
|
18
18
|
storeStatus: StoreStatus;
|
|
@@ -29,6 +29,7 @@ import { z } from "zod";
|
|
|
29
29
|
import { annotate } from "../../shell/field-ui-meta.js";
|
|
30
30
|
import { mediaUrlSchema } from "../../../validation/schemas.js";
|
|
31
31
|
import { StoreStatusValues } from "../../stores/schemas/firestore.js";
|
|
32
|
+
import { ALL_STORE_CAPABILITIES } from "../../auth/permissions/constants.js";
|
|
32
33
|
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
33
34
|
/*
|
|
34
35
|
* `adminAddressFormSchema` is DELETED (W5 / D19).
|
|
@@ -84,7 +85,9 @@ export const adminStoreUpdateSchema = z
|
|
|
84
85
|
section: "standing", order: 4, row: "full", kind: "textarea",
|
|
85
86
|
when: (v) => v.storeStatus === "suspended",
|
|
86
87
|
}),
|
|
87
|
-
|
|
88
|
+
// 🛑 z.enum, not z.string().min(1) — see storeFirestoreSchema. A typo'd
|
|
89
|
+
// capability used to save cleanly and grant nothing.
|
|
90
|
+
capabilities: annotate(z.array(z.enum(ALL_STORE_CAPABILITIES)).max(50).optional(), {
|
|
88
91
|
section: "capabilities", sectionLabel: "Capabilities", order: 1, row: "full", kind: "list",
|
|
89
92
|
}),
|
|
90
93
|
adminNotes: annotate(z.string().trim().max(2000).optional(), {
|
|
@@ -27,6 +27,7 @@ import { RelatedProducts } from "../../products/components/RelatedProducts.js";
|
|
|
27
27
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient.js";
|
|
28
28
|
import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges.js";
|
|
29
29
|
import { FeatureBadgeList } from "../../products/components/FeatureBadge.js";
|
|
30
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
30
31
|
import { HistoryTracker } from "../../history/components/HistoryTracker.js";
|
|
31
32
|
import { ShareButton } from "../../products/components/ShareButton.js";
|
|
32
33
|
import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid.js";
|
|
@@ -149,8 +150,12 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
149
150
|
? p.price
|
|
150
151
|
: 0;
|
|
151
152
|
const minBidIncrementOverride = typeof p.minBidIncrement === "number" ? p.minBidIncrement : undefined;
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
154
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
155
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
156
|
+
const galleryUrls = productImageUrls(p.images);
|
|
157
|
+
const images = galleryUrls.length > 0
|
|
158
|
+
? galleryUrls
|
|
154
159
|
: typeof p.mainImage === "string"
|
|
155
160
|
? [p.mainImage]
|
|
156
161
|
: [];
|
|
@@ -206,11 +211,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
206
211
|
});
|
|
207
212
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "auction", snapshot: {
|
|
208
213
|
title: typeof p.title === "string" ? p.title : undefined,
|
|
209
|
-
thumb: typeof p.mainImage === "string"
|
|
210
|
-
? p.mainImage
|
|
211
|
-
: Array.isArray(p.images) && typeof p.images[0] === "string"
|
|
212
|
-
? p.images[0]
|
|
213
|
-
: undefined,
|
|
214
|
+
thumb: typeof p.mainImage === "string" ? p.mainImage : images[0],
|
|
214
215
|
price: typeof p.price === "number" ? p.price : undefined,
|
|
215
216
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
216
217
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
@@ -74,5 +74,15 @@ export interface MarketplaceAuctionCardProps {
|
|
|
74
74
|
onNavigate?: (href: string) => void;
|
|
75
75
|
wishlistActions?: WishlistToggleActions;
|
|
76
76
|
labels?: MarketplaceAuctionCardLabels;
|
|
77
|
+
/**
|
|
78
|
+
* Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
|
|
79
|
+
* nothing for a visitor who does not own the record.
|
|
80
|
+
*
|
|
81
|
+
* 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
|
|
82
|
+
* inside an `<a>` is invalid HTML that browsers resolve by closing the
|
|
83
|
+
* anchor early, which breaks the CARD's navigation rather than just the
|
|
84
|
+
* button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
85
|
+
*/
|
|
86
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
77
87
|
}
|
|
78
|
-
export declare function MarketplaceAuctionCard({ product, buyoutPrice, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, wishlistActions, labels, }: MarketplaceAuctionCardProps): import("react").JSX.Element;
|
|
88
|
+
export declare function MarketplaceAuctionCard({ product, buyoutPrice, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, wishlistActions, labels, renderOwnerActions, }: MarketplaceAuctionCardProps): import("react").JSX.Element;
|
|
@@ -94,7 +94,7 @@ function formatCountdownLabel(remaining, endedLabel) {
|
|
|
94
94
|
return `${hours}h ${minutes}m ${seconds}s`;
|
|
95
95
|
return `${minutes}m ${seconds}s`;
|
|
96
96
|
}
|
|
97
|
-
export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, wishlistActions, labels, }) {
|
|
97
|
+
export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, wishlistActions, labels, renderOwnerActions, }) {
|
|
98
98
|
const router = useRouter();
|
|
99
99
|
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
100
100
|
const baseVariant = variant === "list" ? "list" : "grid";
|
|
@@ -180,5 +180,12 @@ export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", v
|
|
|
180
180
|
remaining, resolvedBuyoutPrice, inWishlist, wishlistLoading, wishlistActions,
|
|
181
181
|
countdownClass, mergedLabels, handleWishlist, handleNavigate,
|
|
182
182
|
};
|
|
183
|
-
|
|
183
|
+
const card = (_jsxs(BaseListingCard, { isSelected: isSelected, isDisabled: isEnded, variant: baseVariant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [renderAuctionCardHero({ product, auctionHref, currentSrc, hovered, images, imageIndex, hasVideo, isEnded, isEndingSoon, isSelected, selectable, mergedLabels, onSelect, setHovered, handleSelect }), _jsx(BaseListingCard.Info, { variant: baseVariant, children: baseVariant === "list" ? renderAuctionCardInfoList(cardInfoProps) : renderAuctionCardInfoGrid(cardInfoProps) })] }));
|
|
184
|
+
if (!renderOwnerActions)
|
|
185
|
+
return card;
|
|
186
|
+
/*
|
|
187
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
188
|
+
* clears this card's own overlays, which all sit at `z-10`.
|
|
189
|
+
*/
|
|
190
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: renderOwnerActions() })] }));
|
|
184
191
|
}
|
|
@@ -67,8 +67,18 @@ export declare const ROUTE_PERMISSION_MAP: Record<string, Permission>;
|
|
|
67
67
|
*
|
|
68
68
|
* Defaults on store creation: ["suggest_brands", "create_coupons"]
|
|
69
69
|
*/
|
|
70
|
-
export type StoreCapability = "host_auctions" | "host_preorders" | "create_categories" | "suggest_brands" | "create_coupons" | "bulk_listing_import" | "extended_return_window" | "verified_seller" | "featured_placement" | "promotional_banner" | "priority_support" | "multiple_stores" | "custom_store_slug" | "api_access" | "lower_commission_rate" | "early_access_features" | "advanced_analytics" | "whatsapp_catalog_sync";
|
|
71
|
-
|
|
70
|
+
export type StoreCapability = "advanced_listing_types" | "host_auctions" | "host_preorders" | "create_categories" | "suggest_brands" | "create_coupons" | "bulk_listing_import" | "extended_return_window" | "verified_seller" | "featured_placement" | "promotional_banner" | "priority_support" | "multiple_stores" | "custom_store_slug" | "api_access" | "lower_commission_rate" | "early_access_features" | "advanced_analytics" | "whatsapp_catalog_sync";
|
|
71
|
+
export declare const ALL_STORE_CAPABILITIES: StoreCapability[];
|
|
72
|
+
/** Narrow an untrusted string to a real capability. */
|
|
73
|
+
export declare function isStoreCapability(value: unknown): value is StoreCapability;
|
|
74
|
+
/**
|
|
75
|
+
* Default capabilities granted to every new store on creation.
|
|
76
|
+
*
|
|
77
|
+
* 🛑 THE one definition. `AdminStoreEditorView` hardcoded its own copy, which
|
|
78
|
+
* is also the `??` fallback on its load path — so the two could disagree about
|
|
79
|
+
* what a store with no `capabilities` field is entitled to, and the admin
|
|
80
|
+
* editor's copy is the one that gets SAVED BACK. Import this; never restate it.
|
|
81
|
+
*/
|
|
72
82
|
export declare const DEFAULT_STORE_CAPABILITIES: StoreCapability[];
|
|
73
83
|
/**
|
|
74
84
|
* Check whether a permission array includes a required permission.
|
|
@@ -87,6 +97,23 @@ export declare function hasAllPermissions(permissions: Permission[], required: P
|
|
|
87
97
|
* Check whether a store's capabilities include a specific capability.
|
|
88
98
|
*/
|
|
89
99
|
export declare function hasCapability(capabilities: StoreCapability[], required: StoreCapability): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a store's stored `capabilities` array to its effective set.
|
|
102
|
+
*
|
|
103
|
+
* 🛑 The PURE half of `getStoreCapabilities`, extracted so a caller that
|
|
104
|
+
* already holds the store document does not pay a second Firestore read to ask
|
|
105
|
+
* what it is entitled to (Rule #6). Three WhatsApp routes were reading
|
|
106
|
+
* `store.capabilities ?? []` inline for exactly that reason, which made them a
|
|
107
|
+
* second, divergent reader of the field — they skipped the default merge, so
|
|
108
|
+
* they and `getStoreCapabilities` could disagree about the same store.
|
|
109
|
+
*
|
|
110
|
+
* Note the empty-array case falls back to the defaults, matching
|
|
111
|
+
* `getStoreCapabilities`' long-standing `raw.length > 0` test. That is
|
|
112
|
+
* arguably the wrong call — an admin who deliberately unticks everything gets
|
|
113
|
+
* the defaults handed back — but it is the established behaviour and changing
|
|
114
|
+
* it would silently re-grant capabilities on stores that had them removed.
|
|
115
|
+
*/
|
|
116
|
+
export declare function resolveStoreCapabilities(raw: readonly string[] | null | undefined): StoreCapability[];
|
|
90
117
|
/**
|
|
91
118
|
* Groups permission prefixes into human-labeled domains for display —
|
|
92
119
|
* used by the employee-permission editor and the read-only permissions
|