@mohasinac/appkit 4.53.2 → 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/server/features/auth/capabilities.js +7 -5
- package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
- package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
- package/dist/_internal/server/features/search/index-build/sources.js +43 -2
- 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/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 +21 -0
- package/dist/constants/api-endpoints.js +7 -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/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/components/BrandDetailPageView.js +2 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
- 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/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/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/products/api/[id]/route.js +8 -1
- package/dist/features/products/api/route.js +16 -4
- package/dist/features/products/components/AvailabilityScopeHint.js +1 -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 +27 -2
- 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/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/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/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/tester-checklist-seed-data.js +403 -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/server.d.ts +4 -8
- package/dist/server.js +8 -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/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
|
@@ -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(), {
|
|
@@ -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
|
|
@@ -319,7 +319,53 @@ export const ROUTE_PERMISSION_MAP = {
|
|
|
319
319
|
"/admin/maintenance/payment-rollbacks": "admin:maintenance:view-payment-rollbacks",
|
|
320
320
|
"/admin/maintenance/analysis": "admin:maintenance:run-analysis",
|
|
321
321
|
};
|
|
322
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Every capability, as a runtime value.
|
|
324
|
+
*
|
|
325
|
+
* 🛑 `Record<StoreCapability, true>` rather than a plain array, for the same
|
|
326
|
+
* reason `ALL_LISTING_TYPES_MAP` is: adding a member to the union without
|
|
327
|
+
* adding it here is then a COMPILE error, not a silent omission. This is what
|
|
328
|
+
* lets `capabilities` be validated as a `z.enum` instead of
|
|
329
|
+
* `z.array(z.string())` — which accepted any string, so a typo'd capability
|
|
330
|
+
* saved successfully and the gate it was meant to open never opened, silently.
|
|
331
|
+
*
|
|
332
|
+
* It also backs the admin editor's grouping, so a new capability cannot be
|
|
333
|
+
* granted in code and remain unreachable from the admin UI.
|
|
334
|
+
*/
|
|
335
|
+
const STORE_CAPABILITY_MAP = {
|
|
336
|
+
advanced_listing_types: true,
|
|
337
|
+
host_auctions: true,
|
|
338
|
+
host_preorders: true,
|
|
339
|
+
create_categories: true,
|
|
340
|
+
suggest_brands: true,
|
|
341
|
+
create_coupons: true,
|
|
342
|
+
bulk_listing_import: true,
|
|
343
|
+
extended_return_window: true,
|
|
344
|
+
verified_seller: true,
|
|
345
|
+
featured_placement: true,
|
|
346
|
+
promotional_banner: true,
|
|
347
|
+
priority_support: true,
|
|
348
|
+
multiple_stores: true,
|
|
349
|
+
custom_store_slug: true,
|
|
350
|
+
api_access: true,
|
|
351
|
+
lower_commission_rate: true,
|
|
352
|
+
early_access_features: true,
|
|
353
|
+
advanced_analytics: true,
|
|
354
|
+
whatsapp_catalog_sync: true,
|
|
355
|
+
};
|
|
356
|
+
export const ALL_STORE_CAPABILITIES = Object.keys(STORE_CAPABILITY_MAP);
|
|
357
|
+
/** Narrow an untrusted string to a real capability. */
|
|
358
|
+
export function isStoreCapability(value) {
|
|
359
|
+
return typeof value === "string" && value in STORE_CAPABILITY_MAP;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Default capabilities granted to every new store on creation.
|
|
363
|
+
*
|
|
364
|
+
* 🛑 THE one definition. `AdminStoreEditorView` hardcoded its own copy, which
|
|
365
|
+
* is also the `??` fallback on its load path — so the two could disagree about
|
|
366
|
+
* what a store with no `capabilities` field is entitled to, and the admin
|
|
367
|
+
* editor's copy is the one that gets SAVED BACK. Import this; never restate it.
|
|
368
|
+
*/
|
|
323
369
|
export const DEFAULT_STORE_CAPABILITIES = [
|
|
324
370
|
"suggest_brands",
|
|
325
371
|
"create_coupons",
|
|
@@ -352,6 +398,27 @@ export function hasAllPermissions(permissions, required) {
|
|
|
352
398
|
export function hasCapability(capabilities, required) {
|
|
353
399
|
return capabilities.includes(required);
|
|
354
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Resolve a store's stored `capabilities` array to its effective set.
|
|
403
|
+
*
|
|
404
|
+
* 🛑 The PURE half of `getStoreCapabilities`, extracted so a caller that
|
|
405
|
+
* already holds the store document does not pay a second Firestore read to ask
|
|
406
|
+
* what it is entitled to (Rule #6). Three WhatsApp routes were reading
|
|
407
|
+
* `store.capabilities ?? []` inline for exactly that reason, which made them a
|
|
408
|
+
* second, divergent reader of the field — they skipped the default merge, so
|
|
409
|
+
* they and `getStoreCapabilities` could disagree about the same store.
|
|
410
|
+
*
|
|
411
|
+
* Note the empty-array case falls back to the defaults, matching
|
|
412
|
+
* `getStoreCapabilities`' long-standing `raw.length > 0` test. That is
|
|
413
|
+
* arguably the wrong call — an admin who deliberately unticks everything gets
|
|
414
|
+
* the defaults handed back — but it is the established behaviour and changing
|
|
415
|
+
* it would silently re-grant capabilities on stores that had them removed.
|
|
416
|
+
*/
|
|
417
|
+
export function resolveStoreCapabilities(raw) {
|
|
418
|
+
if (Array.isArray(raw) && raw.length > 0)
|
|
419
|
+
return raw;
|
|
420
|
+
return [...DEFAULT_STORE_CAPABILITIES];
|
|
421
|
+
}
|
|
355
422
|
// ============================================================================
|
|
356
423
|
// PERMISSION DOMAINS — display grouping (S-ADMIN-7, 2026-08-19)
|
|
357
424
|
// ============================================================================
|
|
@@ -17,5 +17,15 @@ export interface BlogFeaturedCardProps {
|
|
|
17
17
|
showAuthor?: boolean;
|
|
18
18
|
/** Show the cover image. Defaults to `true`. */
|
|
19
19
|
showThumbnail?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Owner-only pencil / clone / bin, for an admin or whoever owns this record.
|
|
22
|
+
* Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
|
|
23
|
+
*
|
|
24
|
+
* 🛑 Rendered OUTSIDE this card's own `<Link>`/`<TextLink>`, as a sibling.
|
|
25
|
+
* A `<button>` inside an `<a>` is invalid HTML that browsers resolve by
|
|
26
|
+
* closing the anchor early, which breaks the CARD's navigation rather than
|
|
27
|
+
* just the button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
28
|
+
*/
|
|
29
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
20
30
|
}
|
|
21
|
-
export declare function BlogFeaturedCard({ post, href, labels, className, selectable, isSelected, onSelect, showReadTime, showAuthor, showThumbnail, }: BlogFeaturedCardProps): React.JSX.Element;
|
|
31
|
+
export declare function BlogFeaturedCard({ post, href, labels, className, selectable, isSelected, onSelect, showReadTime, showAuthor, showThumbnail, renderOwnerActions, }: BlogFeaturedCardProps): React.JSX.Element;
|