@mohasinac/appkit 4.60.1 → 4.62.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.
Files changed (202) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
  3. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
  4. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
  5. package/dist/_internal/server/features/auctions/actions.d.ts +1 -1
  6. package/dist/_internal/server/features/auctions/actions.js +12 -1
  7. package/dist/_internal/server/features/blog/actions.d.ts +1 -1
  8. package/dist/_internal/server/features/blog/actions.js +12 -1
  9. package/dist/_internal/server/features/brands/actions.d.ts +1 -1
  10. package/dist/_internal/server/features/brands/actions.js +12 -1
  11. package/dist/_internal/server/features/cart/actions.d.ts +1 -1
  12. package/dist/_internal/server/features/cart/actions.js +12 -1
  13. package/dist/_internal/server/features/checkout/actions.js +0 -1
  14. package/dist/_internal/server/features/events/actions.d.ts +1 -1
  15. package/dist/_internal/server/features/events/actions.js +12 -1
  16. package/dist/_internal/server/features/gateway-events/redact.js +14 -2
  17. package/dist/_internal/server/features/history/actions.d.ts +1 -1
  18. package/dist/_internal/server/features/history/actions.js +12 -1
  19. package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
  20. package/dist/_internal/server/features/lottery/actions.js +12 -1
  21. package/dist/_internal/server/features/orders/actions.d.ts +1 -1
  22. package/dist/_internal/server/features/orders/actions.js +12 -1
  23. package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
  24. package/dist/_internal/server/features/orders/service.js +31 -1
  25. package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
  26. package/dist/_internal/server/features/payouts/actions.js +12 -1
  27. package/dist/_internal/server/features/points/award.js +9 -1
  28. package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
  29. package/dist/_internal/server/features/pre-orders/actions.js +12 -1
  30. package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
  31. package/dist/_internal/server/features/prize-draws/actions.js +12 -1
  32. package/dist/_internal/server/features/products/actions.d.ts +1 -1
  33. package/dist/_internal/server/features/products/actions.js +12 -1
  34. package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
  35. package/dist/_internal/server/features/promotions/actions.js +12 -1
  36. package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
  37. package/dist/_internal/server/features/protection-claims/actions.js +21 -12
  38. package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
  39. package/dist/_internal/server/features/raffle/actions.js +12 -1
  40. package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
  41. package/dist/_internal/server/features/refunds/actions.js +14 -2
  42. package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
  43. package/dist/_internal/server/features/reviews/actions.js +12 -1
  44. package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
  45. package/dist/_internal/server/features/wishlist/actions.js +12 -1
  46. package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
  47. package/dist/_internal/server/jobs/core/paymentReconcile.js +93 -76
  48. package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
  49. package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
  50. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
  51. package/dist/_internal/shared/actions/action-registry.js +149 -9
  52. package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
  53. package/dist/_internal/shared/features/promotions/schema.js +7 -0
  54. package/dist/_internal/shared/media/limits.d.ts +10 -0
  55. package/dist/_internal/shared/media/limits.js +10 -0
  56. package/dist/_internal/shared/obligations/deadlines.js +11 -1
  57. package/dist/_internal/shared/tester/window.d.ts +25 -0
  58. package/dist/_internal/shared/tester/window.js +66 -0
  59. package/dist/_internal/shared/types/action-result.js +15 -1
  60. package/dist/client/api/surface-error.js +16 -2
  61. package/dist/client/api/useApiMutation.js +17 -1
  62. package/dist/client.d.ts +1 -1
  63. package/dist/client.js +1 -1
  64. package/dist/constants/api-endpoints.d.ts +3 -0
  65. package/dist/constants/api-endpoints.js +1 -0
  66. package/dist/constants/field-names.d.ts +17 -0
  67. package/dist/constants/field-names.js +23 -0
  68. package/dist/core/server-action.js +19 -3
  69. package/dist/core/unit-of-work.js +15 -1
  70. package/dist/errors/error-mapping.js +26 -1
  71. package/dist/errors/firestore-codes.d.ts +37 -0
  72. package/dist/errors/firestore-codes.js +52 -0
  73. package/dist/errors/index.d.ts +0 -1
  74. package/dist/errors/index.js +12 -2
  75. package/dist/features/account/components/UserOffersPanel.js +5 -3
  76. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  77. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  78. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  79. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  80. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  81. package/dist/features/admin/components/AdminCouponEditorView.js +8 -0
  82. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  83. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
  84. package/dist/features/admin/components/AdminMediaView.js +36 -3
  85. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  86. package/dist/features/admin/components/AdminOffersView.js +2 -1
  87. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  88. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  89. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  90. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  91. package/dist/features/admin/components/DataListingView.js +2 -0
  92. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  93. package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
  94. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  95. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  96. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  97. package/dist/features/admin/repository/notification.repository.js +32 -0
  98. package/dist/features/admin/repository/site-settings.repository.d.ts +2 -1
  99. package/dist/features/admin/repository/site-settings.repository.js +7 -0
  100. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  101. package/dist/features/admin/schemas/action-index-form.js +115 -0
  102. package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
  103. package/dist/features/auth/components/RegisterForm.js +2 -1
  104. package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
  105. package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
  106. package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
  107. package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
  108. package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
  109. package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
  110. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  111. package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
  112. package/dist/features/categories/repository/price-observation.repository.js +21 -2
  113. package/dist/features/events/actions/event-actions.js +4 -1
  114. package/dist/features/events/components/AdminEventEditorView.js +79 -50
  115. package/dist/features/events/components/EventsListPageView.js +2 -1
  116. package/dist/features/events/schemas/firestore.d.ts +17 -0
  117. package/dist/features/events/schemas/firestore.js +1 -0
  118. package/dist/features/events/utils/sort-pinned.d.ts +33 -0
  119. package/dist/features/events/utils/sort-pinned.js +46 -0
  120. package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
  121. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  122. package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
  123. package/dist/features/inventory/components/ReceiveStationView.js +61 -17
  124. package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
  125. package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
  126. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  127. package/dist/features/obligations/repository/obligations.repository.js +10 -1
  128. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  129. package/dist/features/orders/components/RefundRequestView.js +4 -4
  130. package/dist/features/orders/schemas/firestore.d.ts +19 -0
  131. package/dist/features/orders/schemas/firestore.js +19 -0
  132. package/dist/features/orders/schemas/index.d.ts +6 -6
  133. package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
  134. package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
  135. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  136. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  137. package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
  138. package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
  139. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  140. package/dist/features/products/components/ProductDetailActions.js +2 -1
  141. package/dist/features/products/components/ProductForm.js +2 -2
  142. package/dist/features/products/components/ProductGrid.js +1 -1
  143. package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
  144. package/dist/features/promotions/schemas/firestore.d.ts +22 -0
  145. package/dist/features/promotions/schemas/firestore.js +22 -0
  146. package/dist/features/promotions/schemas/index.d.ts +479 -6
  147. package/dist/features/promotions/schemas/index.js +153 -0
  148. package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
  149. package/dist/features/seller/components/FulfillmentView.js +3 -2
  150. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  151. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  152. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  153. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  154. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  155. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  156. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  157. package/dist/features/seller/components/SellerProductsView.js +5 -1
  158. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  159. package/dist/features/seller/components/SellerShippingView.js +2 -1
  160. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  161. package/dist/features/seller/components/SellerSupportView.js +69 -2
  162. package/dist/features/seller/schemas/coupon-form.d.ts +5 -0
  163. package/dist/features/seller/schemas/coupon-form.js +18 -0
  164. package/dist/features/shell/SectionForm.js +35 -9
  165. package/dist/features/shell/build-sections.js +14 -1
  166. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  167. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  168. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  169. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  170. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  171. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  172. package/dist/features/sticker-trace/server.d.ts +2 -0
  173. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  174. package/dist/http/api-handler.js +4 -1
  175. package/dist/index.d.ts +8 -4
  176. package/dist/index.js +6 -4
  177. package/dist/next/routing/route-map.d.ts +4 -2
  178. package/dist/next/routing/route-map.js +19 -1
  179. package/dist/schemas/registry.d.ts +15 -10
  180. package/dist/schemas/sieve.d.ts +3 -3
  181. package/dist/server.d.ts +1 -2
  182. package/dist/server.js +6 -4
  183. package/dist/styles.css +51 -0
  184. package/dist/ui/components/Checkbox.js +10 -2
  185. package/dist/ui/components/DateInput.js +8 -1
  186. package/dist/ui/components/Input.js +22 -2
  187. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  188. package/dist/ui/components/PaginatedSelect.js +0 -0
  189. package/dist/ui/components/Radio.js +8 -1
  190. package/dist/ui/components/Select.js +10 -2
  191. package/dist/ui/components/TagInput.d.ts +8 -1
  192. package/dist/ui/components/TagInput.js +4 -3
  193. package/dist/ui/components/Textarea.js +10 -2
  194. package/dist/ui/forms/FieldCheckbox.js +1 -1
  195. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  196. package/dist/ui/forms/FormErrorList.js +34 -1
  197. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  198. package/dist/ui/forms/apply-zod-issues.js +23 -1
  199. package/package.json +1 -1
  200. package/scripts/seed-cli.mjs +1822 -1812
  201. package/dist/features/tester/utils/phases.d.ts +0 -48
  202. package/dist/features/tester/utils/phases.js +0 -139
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize.js";
4
- import { useApiMutation } from "@mohasinac/appkit/client";
4
+ import { useApiMutation, sortBy, sieveFilter, SIEVE_OP } from "@mohasinac/appkit/client";
5
5
  import React from "react";
6
6
  import { useQuery, useQueryClient } from "@tanstack/react-query";
7
7
  import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect.js";
8
8
  import { CategoryInlineSelect } from "../../seller/components/CategoryInlineSelect.js";
9
- import { Button, Checkbox, Div, Input, Modal, PaginatedSelect, Row, Select, Stack, Text, Textarea, useToast } from "../../../ui/index.js";
9
+ import { Badge, Button, Checkbox, ConfirmDeleteModal, Div, FilterChipGroup, Input, PaginatedSelect, Row, Select, SideDrawer, Span, Stack, Text, Textarea, Toggle, useToast } from "../../../ui/index.js";
10
10
  import { ICON_KEY_OPTIONS } from "./sections/icon-options.js";
11
11
  import { FormErrorSummary, applyZodIssues } from "../../../ui/forms/index.js";
12
12
  import { homepageSectionFormSchema, } from "../schemas/homepage-section-form.js";
@@ -16,10 +16,16 @@ import { apiClient } from "../../../http/index.js";
16
16
  import { ADMIN_ENDPOINTS } from "../../../constants/index.js";
17
17
  import { ROUTES } from "../../../next/routing/route-map.js";
18
18
  import { useAdminSectionsListing } from "../hooks/useAdminSectionsListing.js";
19
- import { DataTable } from "./DataTable.js";
19
+ import { toRecordArray, toRelativeDate } from "../hooks/useAdminListingData.js";
20
+ import { useOptimisticRowFlag } from "../hooks/useOptimisticRowFlag.js";
21
+ import { DataListingView } from "./DataListingView.js";
22
+ import { QuickEditMenu } from "./QuickEditMenu.js";
23
+ import { ALL_TAB } from "../constants/filter-tabs.js";
24
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
20
25
  import { templatesForType, templateById, profileById, } from "./sections/sectionTemplates.js";
21
26
  import { SECTION_TYPE_OPTIONS, SUPPORTED_TYPED_BUILDERS, RESOURCE_SORT_OPTIONS, FAQ_CATEGORY_OPTIONS, DEFAULT_PRODUCTS_BUILDER, DEFAULT_AUCTIONS_BUILDER, DEFAULT_STATS_BUILDER, DEFAULT_PRE_ORDERS_BUILDER, DEFAULT_STORES_BUILDER, DEFAULT_EVENTS_BUILDER, DEFAULT_SOCIAL_FEED_BUILDER, DEFAULT_WELCOME_BUILDER, DEFAULT_TRUST_INDICATORS_BUILDER, DEFAULT_CATEGORIES_BUILDER, DEFAULT_BRANDS_BUILDER, DEFAULT_BANNER_BUILDER, DEFAULT_FEATURES_BUILDER, DEFAULT_REVIEWS_BUILDER, DEFAULT_WHATSAPP_BUILDER, DEFAULT_FAQ_BUILDER, DEFAULT_BLOG_BUILDER, DEFAULT_NEWSLETTER_BUILDER, DEFAULT_CAROUSEL_BUILDER, DEFAULT_CUSTOM_CARDS_BUILDER, DEFAULT_GOOGLE_REVIEWS_BUILDER, DEFAULT_FEATURED_BUNDLES_BUILDER, DEFAULT_PRIZE_DRAWS_BUILDER, DEFAULT_EVENT_RAFFLES_BUILDER, DEFAULT_COLLECTION_CARDS_BUILDER, } from "./sections/adminSectionsTypes.js";
22
27
  import { toStringValue, buildProductsConfig, buildAuctionsConfig, buildStatsConfig, buildPreOrdersConfig, buildStoresConfig, buildEventsConfig, buildSocialFeedConfig, buildWelcomeConfig, buildTrustIndicatorsConfig, buildCategoriesConfig, buildBrandsConfig, buildBannerConfig, buildFeaturesConfig, buildReviewsConfig, buildWhatsAppConfig, buildFAQConfig, buildBlogConfig, buildNewsletterConfig, buildCarouselConfig, buildCustomCardsConfig, buildGoogleReviewsConfig, buildFeaturedBundlesConfig, buildPrizeDrawsConfig, buildEventRafflesConfig, buildCollectionCardsConfig, parseProductsBuilder, parseAuctionsBuilder, parseStatsBuilder, parsePreOrdersBuilder, parseStoresBuilder, parseEventsBuilder, parseSocialFeedBuilder, parseWelcomeBuilder, parseTrustIndicatorsBuilder, parseCategoriesBuilder, parseBrandsBuilder, parseBannerBuilder, parseFeaturesBuilder, parseReviewsBuilder, parseWhatsAppBuilder, parseFAQBuilder, parseBlogBuilder, parseNewsletterBuilder, parseCarouselBuilder, parseCustomCardsBuilder, parseGoogleReviewsBuilder, parseFeaturedBundlesBuilder, parsePrizeDrawsBuilder, parseEventRafflesBuilder, parseCollectionCardsBuilder } from "./sections/adminSectionsBuildParse.js";
28
+ import { toUserMessage } from "../../../errors/error-display-map.js";
23
29
  const __P = {
24
30
  p3: "p-[var(--appkit-space-3)]",
25
31
  p4: "p-[var(--appkit-space-4)]",
@@ -35,12 +41,28 @@ const LBL_SCROLL_INTERVAL = "Scroll interval (ms)";
35
41
  const LBL_VIEW_MORE = "View all link label";
36
42
  const PH_VIEW_MORE = "View all →";
37
43
  const HELP_VIEW_MORE = "Leave blank to use the default label.";
38
- export function AdminSectionsView({ children }) {
39
- const hasChildren = React.Children.count(children) > 0;
44
+ /**
45
+ * The homepage-section editor: the envelope, 25 typed builders and the raw
46
+ * JSON fallback.
47
+ *
48
+ * 🛑 THIS IS A SEPARATE COMPONENT FOR A MECHANICAL REASON, NOT FOR TIDINESS.
49
+ * `usePanelUrlSync` lives inside `useAdminListing` inside `DataListingView`,
50
+ * so the id of the row whose pencil was clicked is only readable inside the
51
+ * `renderEditor(ctx)` callback. The hydrate effect below must run in a
52
+ * component body, so the editor has to BE a component.
53
+ *
54
+ * 🛑 The caller MUST pass `key={ctx.editId ?? "create"}`. `isCreateOpen ||
55
+ * isEditOpen` is a single boolean, so clicking row B's pencil while row A's
56
+ * drawer is open does NOT unmount this component — `editId` simply changes.
57
+ * Without the key, `hydratedRef` below sees "already hydrated" and the admin
58
+ * edits B's record with A's values, then saves it. Silent cross-record
59
+ * corruption, no error anywhere. The remount the key forces is also what
60
+ * resets the 24 non-matching builders and `selectedTemplateId`, which the old
61
+ * single-component version had to do by hand and did not.
62
+ */
63
+ function SectionEditorPanel({ sectionId, section, siblingSections, isSectionsLoading, onDone, }) {
64
+ const isCreate = !sectionId;
40
65
  const queryClient = useQueryClient();
41
- const [isModalOpen, setIsModalOpen] = React.useState(false);
42
- const [mode, setMode] = React.useState("create");
43
- const [selectedSectionId, setSelectedSectionId] = React.useState("");
44
66
  const [sectionType, setSectionType] = React.useState("products");
45
67
  /*
46
68
  * D1 — which template the admin picked, or "" for the type's defaults.
@@ -51,7 +73,19 @@ export function AdminSectionsView({ children }) {
51
73
  const [selectedTemplateId, setSelectedTemplateId] = React.useState("");
52
74
  const [enabled, setEnabled] = React.useState(true);
53
75
  const [order, setOrder] = React.useState("");
54
- const [configJson, setConfigJson] = React.useState("{}");
76
+ /*
77
+ * The RAW textarea contents — meaningful only for a section type with no
78
+ * typed builder. For a typed builder `configJson` below is DERIVED from
79
+ * `typedConfig`, because the textarea is `readOnly` in that case and was
80
+ * therefore never an input: it is a preview.
81
+ *
82
+ * 🛑 This used to be plain state written by an effect on `typedConfig`,
83
+ * which ran AFTER the hydrate effect and overwrote the stored document with
84
+ * a builder-generated config on every open. Deriving removes the race by
85
+ * construction rather than by ordering, and removes a second render per
86
+ * builder keystroke.
87
+ */
88
+ const [rawConfigJson, setRawConfigJson] = React.useState("{}");
55
89
  const [productsBuilder, setProductsBuilder] = React.useState(DEFAULT_PRODUCTS_BUILDER);
56
90
  const [auctionsBuilder, setAuctionsBuilder] = React.useState(DEFAULT_AUCTIONS_BUILDER);
57
91
  const [statsBuilder, setStatsBuilder] = React.useState(DEFAULT_STATS_BUILDER);
@@ -77,16 +111,9 @@ export function AdminSectionsView({ children }) {
77
111
  const [prizeDrawsBuilder, setPrizeDrawsBuilder] = React.useState(DEFAULT_PRIZE_DRAWS_BUILDER);
78
112
  const [eventRafflesBuilder, setEventRafflesBuilder] = React.useState(DEFAULT_EVENT_RAFFLES_BUILDER);
79
113
  const [collectionCardsBuilder, setCollectionCardsBuilder] = React.useState(DEFAULT_COLLECTION_CARDS_BUILDER);
80
- const [reorderDraft, setReorderDraft] = React.useState([]);
81
- const [reorderServerSnapshot, setReorderServerSnapshot] = React.useState([]);
82
- const [reorderUndoStack, setReorderUndoStack] = React.useState([]);
83
- const [dragIndex, setDragIndex] = React.useState(null);
84
114
  const toast = useToast();
85
115
  const isTypedBuilder = SUPPORTED_TYPED_BUILDERS.includes(sectionType);
86
- const { sections, isLoading, errorMessage } = useAdminSectionsListing({
87
- page: 1,
88
- pageSize: 50,
89
- });
116
+ const duplicateOfType = React.useMemo(() => (isCreate ? siblingSections.find((s) => s.type === sectionType) : undefined), [isCreate, siblingSections, sectionType]);
90
117
  const categoriesQuery = useQuery({
91
118
  queryKey: ["admin", "sections", "categories", "options"],
92
119
  queryFn: () => apiClient.get(`${ADMIN_ENDPOINTS.CATEGORIES}?page=1&pageSize=100&sorts=name`),
@@ -165,25 +192,15 @@ export function AdminSectionsView({ children }) {
165
192
  eventRafflesBuilder,
166
193
  collectionCardsBuilder,
167
194
  ]);
168
- React.useEffect(() => {
169
- if (!typedConfig) {
170
- return;
171
- }
172
- setConfigJson(JSON.stringify(typedConfig, null, 2));
173
- }, [typedConfig]);
174
- React.useEffect(() => {
175
- const sorted = [...sections]
176
- .sort((a, b) => a.order - b.order)
177
- .map((section) => ({
178
- id: section.id,
179
- type: section.type,
180
- order: section.order,
181
- }));
182
- const normalized = normalizeOrder(sorted);
183
- setReorderDraft(normalized);
184
- setReorderServerSnapshot(normalized);
185
- setReorderUndoStack([]);
186
- }, [sections]);
195
+ /**
196
+ * What the Generated-config preview shows and what the save sends.
197
+ *
198
+ * Derived for a typed builder, raw state otherwise. `typedConfig` is null
199
+ * for the three types with no builder (`how-it-works`, `testimonials`,
200
+ * `promo-grid`), which is the same set `isTypedBuilder` is false for — so
201
+ * the two branches agree and neither can strand the other.
202
+ */
203
+ const configJson = React.useMemo(() => (isTypedBuilder && typedConfig ? JSON.stringify(typedConfig, null, 2) : rawConfigJson), [isTypedBuilder, typedConfig, rawConfigJson]);
187
204
  const saveSection = useApiMutation({
188
205
  errorMessage: "Failed to save section.",
189
206
  mutationFn: async () => {
@@ -195,7 +212,7 @@ export function AdminSectionsView({ children }) {
195
212
  void normalizeError(_err);
196
213
  throw new Error("Config must be valid JSON.");
197
214
  }
198
- if (mode === "create") {
215
+ if (isCreate) {
199
216
  await apiClient.post(ADMIN_ENDPOINTS.SECTIONS, {
200
217
  type: sectionType,
201
218
  enabled,
@@ -204,9 +221,17 @@ export function AdminSectionsView({ children }) {
204
221
  });
205
222
  return;
206
223
  }
207
- if (!selectedSectionId) {
208
- throw new Error("Select a section to update.");
209
- }
224
+ /*
225
+ * `config` is sent unconditionally and that is deliberate. The
226
+ * repository MERGES it (`{...existing.config, ...data.config}`), which
227
+ * costs one extra read — the price of a guaranteed-correct write. A
228
+ * "diff and skip" would save that read and silently drop an admin's
229
+ * edit whenever the comparison was wrong, which is the worse failure.
230
+ *
231
+ * The reads worth saving are the per-ROW ones, and those already omit
232
+ * `config`: the enable/disable toggle and the reorder save both send a
233
+ * single scalar and short-circuit the merge.
234
+ */
210
235
  const payload = {
211
236
  enabled,
212
237
  config: parsedConfig,
@@ -214,44 +239,39 @@ export function AdminSectionsView({ children }) {
214
239
  if (order.trim()) {
215
240
  payload.order = Number(order);
216
241
  }
217
- await apiClient.patch(`${ADMIN_ENDPOINTS.SECTIONS}/${selectedSectionId}`, payload);
218
- },
219
- onSuccess: async () => {
220
- await queryClient.invalidateQueries({ queryKey: ["admin", "sections", "listing"] });
221
- // Public-facing useHomepageSections() reads under a separate key.
222
- await queryClient.invalidateQueries({ queryKey: ["homepage-sections"] });
223
- toast.showToast(mode === "create" ? "Section created." : "Section updated.", "success");
224
- setIsModalOpen(false);
225
- },
226
- });
227
- const reorderSections = useApiMutation({
228
- errorMessage: "Failed to reorder sections.",
229
- mutationFn: async () => {
230
- await Promise.all(reorderDraft.map((item) => apiClient.patch(`${ADMIN_ENDPOINTS.SECTIONS}/${item.id}`, {
231
- order: item.order,
232
- })));
242
+ await apiClient.patch(`${ADMIN_ENDPOINTS.SECTIONS}/${sectionId}`, payload);
233
243
  },
234
244
  onSuccess: async () => {
235
245
  await queryClient.invalidateQueries({ queryKey: ["admin", "sections", "listing"] });
236
246
  // Public-facing useHomepageSections() reads under a separate key.
237
247
  await queryClient.invalidateQueries({ queryKey: ["homepage-sections"] });
238
- setReorderServerSnapshot(cloneReorderItems(reorderDraft));
239
- setReorderUndoStack([]);
240
- toast.showToast("Section order updated.", "success");
248
+ toast.showToast(isCreate ? "Section created." : "Section updated.", "success");
249
+ onDone();
241
250
  },
242
251
  });
252
+ /**
253
+ * Hydrate the builders from the stored document — ONCE per mounted record.
254
+ *
255
+ * 🛑 `hydratedRef` is not an optimisation. The old deps included the whole
256
+ * `sections` array, which is a fresh identity after every refetch — and with
257
+ * `refetchOnWindowFocus` plus a 30s `staleTime`, alt-tabbing away mid-edit
258
+ * and coming back re-ran this and silently reverted every builder change the
259
+ * admin had made. No error, no prompt, just their work gone.
260
+ *
261
+ * The ref is only correct because the caller remounts on `editId` (see the
262
+ * 🛑 on this component). Ref and key are a pair; neither works alone.
263
+ */
264
+ const hydratedRef = React.useRef(false);
243
265
  React.useEffect(() => {
244
- if (mode !== "edit" || !selectedSectionId)
245
- return;
246
- const selected = sections.find((section) => section.id === selectedSectionId);
247
- if (!selected)
266
+ if (!section || hydratedRef.current)
248
267
  return;
249
- setSectionType(selected.type);
250
- setEnabled(Boolean(selected.enabled));
251
- setOrder(String(selected.order ?? ""));
252
- const selectedConfig = (selected.config ?? {});
253
- setConfigJson(JSON.stringify(selectedConfig, null, 2));
254
- switch (selected.type) {
268
+ hydratedRef.current = true;
269
+ setSectionType(section.type);
270
+ setEnabled(Boolean(section.enabled));
271
+ setOrder(String(section.order ?? ""));
272
+ const selectedConfig = (section.config ?? {});
273
+ setRawConfigJson(JSON.stringify(selectedConfig, null, 2));
274
+ switch (section.type) {
255
275
  case "products":
256
276
  setProductsBuilder(parseProductsBuilder(selectedConfig));
257
277
  break;
@@ -328,7 +348,7 @@ export function AdminSectionsView({ children }) {
328
348
  setCollectionCardsBuilder(parseCollectionCardsBuilder(selectedConfig));
329
349
  break;
330
350
  }
331
- }, [mode, sections, selectedSectionId]);
351
+ }, [section]);
332
352
  /**
333
353
  * D1 — set the builder for one section type, from a TEMPLATE or from the
334
354
  * type's default.
@@ -434,11 +454,11 @@ export function AdminSectionsView({ children }) {
434
454
  * template rather than its own 25-arm switch.
435
455
  */
436
456
  React.useEffect(() => {
437
- if (!isModalOpen || mode !== "create")
457
+ if (!isCreate)
438
458
  return;
439
459
  applyBuilderFor(sectionType);
440
460
  setSelectedTemplateId("");
441
- }, [isModalOpen, mode, sectionType, applyBuilderFor]);
461
+ }, [isCreate, sectionType, applyBuilderFor]);
442
462
  function toggleCategorySelection(ids, categoryId, checked) {
443
463
  if (checked) {
444
464
  if (ids.includes(categoryId)) {
@@ -448,90 +468,6 @@ export function AdminSectionsView({ children }) {
448
468
  }
449
469
  return ids.filter((id) => id !== categoryId);
450
470
  }
451
- function normalizeOrder(items) {
452
- return items.map((item, index) => ({ ...item, order: index + 1 }));
453
- }
454
- function cloneReorderItems(items) {
455
- return items.map((item) => ({ ...item }));
456
- }
457
- function isSameReorder(a, b) {
458
- if (a.length !== b.length) {
459
- return false;
460
- }
461
- return a.every((item, index) => {
462
- const other = b[index];
463
- return Boolean(other) && item.id === other.id && item.order === other.order;
464
- });
465
- }
466
- function pushUndoSnapshot(snapshot) {
467
- setReorderUndoStack((history) => [cloneReorderItems(snapshot), ...history].slice(0, 30));
468
- }
469
- function applyReorderChange(transform) {
470
- setReorderDraft((prev) => {
471
- const next = transform(prev);
472
- if (isSameReorder(prev, next)) {
473
- return prev;
474
- }
475
- pushUndoSnapshot(prev);
476
- return next;
477
- });
478
- }
479
- function moveReorderItem(index, direction) {
480
- const targetIndex = index + direction;
481
- if (targetIndex < 0 || targetIndex >= reorderDraft.length) {
482
- return;
483
- }
484
- applyReorderChange((prev) => {
485
- const next = [...prev];
486
- const [item] = next.splice(index, 1);
487
- next.splice(targetIndex, 0, item);
488
- return normalizeOrder(next);
489
- });
490
- }
491
- function updateReorderItemOrder(index, nextOrder) {
492
- applyReorderChange((prev) => {
493
- const clamped = Math.min(Math.max(1, nextOrder), prev.length);
494
- const currentIndex = index;
495
- const sorted = [...prev];
496
- const [item] = sorted.splice(currentIndex, 1);
497
- sorted.splice(clamped - 1, 0, item);
498
- return normalizeOrder(sorted);
499
- });
500
- }
501
- function moveItemToIndex(fromIndex, toIndex) {
502
- if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0) {
503
- return;
504
- }
505
- applyReorderChange((prev) => {
506
- if (fromIndex >= prev.length || toIndex >= prev.length) {
507
- return prev;
508
- }
509
- const next = [...prev];
510
- const [item] = next.splice(fromIndex, 1);
511
- next.splice(toIndex, 0, item);
512
- return normalizeOrder(next);
513
- });
514
- }
515
- function handleReorderDrop(targetIndex) {
516
- if (dragIndex !== null) {
517
- moveItemToIndex(dragIndex, targetIndex);
518
- }
519
- setDragIndex(null);
520
- }
521
- function reindexDraft() {
522
- applyReorderChange((prev) => normalizeOrder(prev));
523
- }
524
- function resetToServerOrder() {
525
- applyReorderChange(() => cloneReorderItems(reorderServerSnapshot));
526
- }
527
- function undoReorderChange() {
528
- if (reorderUndoStack.length === 0) {
529
- return;
530
- }
531
- const [latest, ...rest] = reorderUndoStack;
532
- setReorderUndoStack(rest);
533
- setReorderDraft(cloneReorderItems(latest));
534
- }
535
471
  function renderProductsCategorySelectors() {
536
472
  return (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Category selectors" }), categoriesQuery.isLoading ? (_jsx(Text, { size: "sm", color: "muted", children: "Loading categories..." })) : categoryOptions.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No categories available." })) : (_jsx(Div, { layout: "grid", gap: "2", className: "grid-cols-1 sm:grid-cols-2", children: categoryOptions.map((category) => (_jsx(Checkbox, { checked: productsBuilder.selectedCategoryIds.includes(category.id), label: category.name, onChange: (event) => setProductsBuilder((prev) => ({
537
473
  ...prev,
@@ -968,22 +904,6 @@ export function AdminSectionsView({ children }) {
968
904
  default: return null;
969
905
  }
970
906
  }
971
- // React.useMemo must run unconditionally on every render — it was
972
- // previously declared after the `hasChildren` early return below, which
973
- // skips the hook call in passthrough mode, violating the Rules of Hooks.
974
- const sectionOrderMap = React.useMemo(() => {
975
- return new Map(sections.map((section) => [section.id, section.order]));
976
- }, [sections]);
977
- // Map sections data to listing rows
978
- const rows = sections.map((section) => ({
979
- id: section.id,
980
- primary: section.type.charAt(0).toUpperCase() + section.type.slice(1).replace(/-/g, " "),
981
- secondary: `Order: ${section.order} • ${section.enabled ? "Enabled" : "Disabled"}`,
982
- status: section.enabled ? "Active" : "Inactive",
983
- updatedAt: new Date(section.updatedAt).toLocaleDateString(),
984
- }));
985
- const hasReorderChanges = reorderDraft.some((item) => sectionOrderMap.get(item.id) !== item.order);
986
- const canUndoReorderChanges = reorderUndoStack.length > 0;
987
907
  /**
988
908
  * The modal's fields, as three hand-written sections.
989
909
  *
@@ -1004,21 +924,12 @@ export function AdminSectionsView({ children }) {
1004
924
  label: "Section",
1005
925
  required: true,
1006
926
  fields: ["sectionType", "order", "enabled"],
1007
- render: () => (_jsxs(Stack, { gap: "md", children: [_jsx(Select, { label: "Mode", value: mode, onValueChange: (value) => setMode(value), options: [
1008
- { label: "Create new section", value: "create" },
1009
- { label: "Edit existing section", value: "edit" },
1010
- ] }), mode === "edit" ? (_jsx(Select, { label: "Section", value: selectedSectionId, onValueChange: setSelectedSectionId, placeholder: "Select section", options: sections.map((section) => ({
1011
- label: `${section.type} (#${section.order})`,
1012
- value: section.id,
1013
- })) })) : null, _jsx(Select, { label: "Section type", value: sectionType, onValueChange: (value) => {
1014
- setSectionType(value);
1015
- if (mode === "create") {
1016
- setSelectedSectionId("");
1017
- }
1018
- }, options: SECTION_TYPE_OPTIONS.map((type) => ({
927
+ render: () => (_jsxs(Stack, { gap: "md", children: [_jsx(Select, { label: "Section type", value: sectionType, onValueChange: (value) => setSectionType(value), options: SECTION_TYPE_OPTIONS.map((type) => ({
1019
928
  label: type,
1020
929
  value: type,
1021
- })), disabled: mode === "edit" }), mode === "create" && templatesForType(sectionType).length > 0 && (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Start from a template", value: selectedTemplateId, onValueChange: (value) => {
930
+ })), disabled: !isCreate, helperText: isCreate
931
+ ? undefined
932
+ : "A section's type is fixed — its config has a different shape per type." }), isCreate && duplicateOfType && (_jsxs(Text, { size: "xs", color: "warning", children: ["A ", sectionType, " section already exists on this surface (#", duplicateOfType.order, "). Every enabled section is rendered, so a second one shows this type twice."] })), isCreate && templatesForType(sectionType).length > 0 && (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Start from a template", value: selectedTemplateId, onValueChange: (value) => {
1022
933
  setSelectedTemplateId(value);
1023
934
  // "" means the type's own defaults — `applyBuilderFor` takes
1024
935
  // no template and behaves exactly as the reset effect does.
@@ -1032,7 +943,9 @@ export function AdminSectionsView({ children }) {
1032
943
  return null;
1033
944
  const p = profileById(t.sourceProfileId);
1034
945
  return (_jsxs(Div, { surface: "muted", padding: "sm", rounded: "md", children: [t.notes && _jsx(Text, { size: "xs", color: "muted", children: t.notes }), p && (_jsxs(Text, { size: "xs", color: "faint", children: [p.label, " \u00B7 ", p.platform, " \u00B7 ", p.measured.products.toLocaleString(), " products \u00B7", " ", Math.round(p.measured.soldOutShare * 100), "% sold out", p.measured.uniqueInternalLinks ? ` · ${p.measured.uniqueInternalLinks} internal links` : ""] }))] }));
1035
- })()] })), _jsx(Input, { label: "Order", type: "number", value: order, onChange: (event) => setOrder(event.target.value), placeholder: "Leave empty to auto-place" }), _jsx(Select, { label: "Enabled", value: enabled ? "true" : "false", onValueChange: (value) => setEnabled(value === "true"), options: [
946
+ })()] })), _jsx(Input, { label: "Order", type: "number", value: order, onChange: (event) => setOrder(event.target.value), placeholder: isCreate ? "Leave empty to auto-place" : "Leave empty to keep the current order", helperText: isCreate
947
+ ? undefined
948
+ : "Reorder sections in the toolbar is the easier way to move several at once." }), _jsx(Select, { label: "Enabled", value: enabled ? "true" : "false", onValueChange: (value) => setEnabled(value === "true"), options: [
1036
949
  { label: "Enabled", value: "true" },
1037
950
  { label: "Disabled", value: "false" },
1038
951
  ] })] })),
@@ -1047,13 +960,21 @@ export function AdminSectionsView({ children }) {
1047
960
  id: "config",
1048
961
  label: isTypedBuilder ? "Generated config" : "Section config (JSON)",
1049
962
  fields: ["configJson"],
1050
- render: () => (_jsx(Textarea, { label: isTypedBuilder ? "Generated config (JSON preview)" : "Section config (JSON)", value: configJson, onChange: (event) => setConfigJson(event.target.value), rows: 10, readOnly: isTypedBuilder, helperText: isTypedBuilder
1051
- ? "This JSON is generated from typed controls above."
963
+ render: () => (_jsx(Textarea, { label: isTypedBuilder ? "Generated config (JSON preview)" : "Section config (JSON)", value: configJson, onChange: (event) => setRawConfigJson(event.target.value), rows: 10, readOnly: isTypedBuilder, helperText: isTypedBuilder
964
+ ? "Generated from the typed controls above. It shows only the keys those controls know about — any other key already stored on this section is preserved on save and simply isn't shown here."
1052
965
  : "Provide section config JSON for this section type." })),
1053
966
  },
967
+ /*
968
+ * `selectedTemplateId` and `applyBuilderFor` are read by the template
969
+ * picker and were both missing from this list. The notes panel under the
970
+ * picker refreshed anyway — but only by ACCIDENT, because picking a
971
+ * template also mutates builder state, which changes `configJson`, which
972
+ * is in the list. Naming them makes the panel's correctness depend on the
973
+ * thing it actually reads.
974
+ */
1054
975
  ], [
1055
- mode, selectedSectionId, sections, sectionType, order, enabled,
1056
- isTypedBuilder, configJson, renderTypedBuilder,
976
+ isCreate, sectionType, order, enabled, duplicateOfType, selectedTemplateId,
977
+ applyBuilderFor, isTypedBuilder, configJson, renderTypedBuilder,
1057
978
  ]);
1058
979
  /** What the nav and the error summary key on. */
1059
980
  const draft = React.useMemo(() => ({ sectionType, order, enabled, configJson }), [sectionType, order, enabled, configJson]);
@@ -1064,44 +985,385 @@ export function AdminSectionsView({ children }) {
1064
985
  fieldToSectionIndex: nav.fieldToSectionIndex,
1065
986
  });
1066
987
  /*
1067
- * Passthrough mode (detail view).
988
+ * A deep link (`?panel=edit&id=…`) can name a section that no longer
989
+ * exists — the delete confirm may have been accepted in another tab while
990
+ * this URL sat in history. Say so, rather than rendering a form bound to
991
+ * nothing and letting Save create a section the admin meant to edit.
992
+ */
993
+ if (sectionId && !section && !isSectionsLoading) {
994
+ return (_jsxs(Stack, { gap: "md", padding: "md", children: [_jsx(Text, { size: "sm", color: "muted", children: "This section no longer exists \u2014 it may have been deleted." }), _jsx(Row, { children: _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onDone, children: "Close" }) })] }));
995
+ }
996
+ return (_jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: modalSections, values: draft,
997
+ /*
998
+ * Each control writes through its own setter — `setSectionType`,
999
+ * `setOrder`, `setEnabled` — and several do more than store a value.
1000
+ * So this routes nothing generically.
1001
+ */
1002
+ onChange: () => undefined, onSubmit: () => {
1003
+ form.clearErrors();
1004
+ const parsed = homepageSectionFormSchema.safeParse(draft);
1005
+ if (!parsed.success) {
1006
+ applyZodIssues(parsed.error.issues, form.setFieldError);
1007
+ return;
1008
+ }
1009
+ saveSection.mutate();
1010
+ }, onValidationChange: () => form.validate(draft), schema: homepageSectionFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: saveSection.isPending
1011
+ ? "Saving..."
1012
+ : isCreate
1013
+ ? "Create section"
1014
+ : "Update section", cancelLabel: "Cancel", onCancel: onDone, isLoading: saveSection.isPending })] }));
1015
+ }
1016
+ /*
1017
+ * 🛑 Not registered in `filter-tabs.ts`. `audit-filter-tab-enums` cross-checks
1018
+ * chip ids against a backing STRING enum, and `enabled` is a boolean — the
1019
+ * same out-of-scope category as ADMIN_CART_OWNERSHIP_TABS.
1020
+ */
1021
+ const SECTION_ENABLED_TABS = [
1022
+ ALL_TAB,
1023
+ { id: "true", label: "Enabled" },
1024
+ { id: "false", label: "Disabled" },
1025
+ ];
1026
+ /** Reorder renumbers 1..N GLOBALLY — see the full-set query in the component. */
1027
+ function normalizeOrder(items) {
1028
+ return items.map((item, index) => ({ ...item, order: index + 1 }));
1029
+ }
1030
+ function cloneReorderItems(items) {
1031
+ return items.map((item) => ({ ...item }));
1032
+ }
1033
+ function isSameReorder(a, b) {
1034
+ if (a.length !== b.length)
1035
+ return false;
1036
+ return a.every((item, index) => {
1037
+ const other = b[index];
1038
+ return Boolean(other) && item.id === other.id && item.order === other.order;
1039
+ });
1040
+ }
1041
+ /**
1042
+ * The homepage/surface sections listing.
1043
+ *
1044
+ * Was a bare `<DataTable rows>` with no `columns`, no `renderRowActions`, no
1045
+ * `onRowClick` and no `rowHrefTemplate` — so `isInteractive` was false and
1046
+ * every row was inert, including the blank avatar circles that come from
1047
+ * `buildDefaultColumns(undefined)`. The only way to edit a section was a
1048
+ * toolbar button, a "Mode" dropdown, and a second dropdown to re-find the row
1049
+ * you had just clicked past.
1050
+ */
1051
+ export function AdminSectionsView({ children }) {
1052
+ const hasChildren = React.Children.count(children) > 0;
1053
+ const queryClient = useQueryClient();
1054
+ const toast = useToast();
1055
+ const [reorderOpen, setReorderOpen] = React.useState(false);
1056
+ const [reorderDraft, setReorderDraft] = React.useState([]);
1057
+ const [reorderServerSnapshot, setReorderServerSnapshot] = React.useState([]);
1058
+ const [reorderUndoStack, setReorderUndoStack] = React.useState([]);
1059
+ const [dragIndex, setDragIndex] = React.useState(null);
1060
+ const [deleteTargetId, setDeleteTargetId] = React.useState(null);
1061
+ /*
1062
+ * 🛑 `config` is deliberately omitted from the PATCH:
1063
+ * `HomepageSectionsRepository.update` short-circuits its merge READ when
1064
+ * `data.config` is absent, so an enable/disable costs one write and no read.
1068
1065
  *
1069
- * 🛑 The short-circuit sits HERE, after every hook, rather than ~170 lines
1070
- * up where it used to. Four hooks had accumulated below it — `modalSections`,
1071
- * `draft`, `nav`, `form` — so passing children skipped all four and the hook
1072
- * ORDER changed between the two render paths, which is how React ends up
1073
- * reading one hook's state out of another's slot. The comment on
1074
- * `sectionOrderMap` above is the record of this same bug being fixed once
1075
- * already, one hook at a time.
1066
+ * The optimistic/rollback logic lives in the shared hook — see its header
1067
+ * for why a rollback must DELETE the override key rather than write back a
1068
+ * captured value, and why the key is cleared on success too.
1069
+ */
1070
+ const enabledFlag = useOptimisticRowFlag({
1071
+ mutate: (id, next) => apiClient.patch(`${ADMIN_ENDPOINTS.SECTIONS}/${id}`, { enabled: next }),
1072
+ // The public homepage reads under a key of its own; both must be busted.
1073
+ invalidate: [["admin", "sections", "listing"], ["homepage-sections"]],
1074
+ successMessage: (next) => (next ? "Section enabled." : "Section disabled."),
1075
+ errorMessage: "Failed to update section.",
1076
+ });
1077
+ /*
1078
+ * The reorder panel needs EVERY section, not the current page.
1079
+ * `normalizeOrder` assigns `index + 1` globally, so feeding it the
1080
+ * DataListingView rows — which are paginated, filtered and sortable — would
1081
+ * renumber a partial set 1..N and PATCH wrong orders onto real rows.
1082
+ */
1083
+ const { sections, total, isLoading } = useAdminSectionsListing({ page: 1, pageSize: 50 });
1084
+ const isReorderTruncated = total > sections.length;
1085
+ const sectionOrderMap = React.useMemo(() => new Map(sections.map((section) => [section.id, section.order])), [sections]);
1086
+ React.useEffect(() => {
1087
+ const sorted = [...sections]
1088
+ .sort((a, b) => a.order - b.order)
1089
+ .map((section) => ({ id: section.id, type: section.type, order: section.order }));
1090
+ setReorderDraft(normalizeOrder(sorted));
1091
+ /*
1092
+ * 🛑 The RAW server orders, not the normalized ones.
1093
+ *
1094
+ * Orders can legitimately have a gap — a delete leaves one — and
1095
+ * snapshotting the normalized list claims the server holds 1..N when it
1096
+ * does not. "Reset to server" would then reset to an order the server
1097
+ * never had, and `hasReorderChanges` (which compares against the real
1098
+ * `sectionOrderMap`) could not agree with it. Unreachable while every row
1099
+ * was 1..N; reachable the moment delete shipped.
1100
+ */
1101
+ setReorderServerSnapshot(sorted);
1102
+ setReorderUndoStack([]);
1103
+ }, [sections]);
1104
+ const invalidateSections = React.useCallback(async () => {
1105
+ try {
1106
+ await queryClient.invalidateQueries({ queryKey: ["admin", "sections", "listing"] });
1107
+ // Public-facing useHomepageSections() reads under a separate key.
1108
+ await queryClient.invalidateQueries({ queryKey: ["homepage-sections"] });
1109
+ }
1110
+ catch (err) {
1111
+ /*
1112
+ * A refetch that fails after a SUCCESSFUL write is not a failed write,
1113
+ * so this must not raise — doing so would report "Failed to delete
1114
+ * section" for a section that is gone. The list is stale rather than
1115
+ * wrong, and says so.
1116
+ */
1117
+ const normalized = normalizeError(err);
1118
+ toast.showToast(toUserMessage(normalized.code, undefined, { fallback: "Saved, but the list could not refresh. Reload to see it." }), "warning");
1119
+ }
1120
+ }, [queryClient, toast]);
1121
+ const deleteSection = useApiMutation({
1122
+ errorMessage: "Failed to delete section.",
1123
+ mutationFn: async (id) => {
1124
+ await apiClient.delete(`${ADMIN_ENDPOINTS.SECTIONS}/${id}`);
1125
+ },
1126
+ onSuccess: async () => {
1127
+ await invalidateSections();
1128
+ toast.showToast("Section deleted.", "success");
1129
+ },
1130
+ });
1131
+ const reorderSections = useApiMutation({
1132
+ errorMessage: "Failed to reorder sections.",
1133
+ mutationFn: async () => {
1134
+ await Promise.all(reorderDraft.map((item) => apiClient.patch(`${ADMIN_ENDPOINTS.SECTIONS}/${item.id}`, { order: item.order })));
1135
+ },
1136
+ onSuccess: async () => {
1137
+ await invalidateSections();
1138
+ setReorderServerSnapshot(cloneReorderItems(reorderDraft));
1139
+ setReorderUndoStack([]);
1140
+ toast.showToast("Section order updated.", "success");
1141
+ },
1142
+ });
1143
+ const pushUndoSnapshot = React.useCallback((snapshot) => {
1144
+ setReorderUndoStack((history) => [cloneReorderItems(snapshot), ...history].slice(0, 30));
1145
+ }, []);
1146
+ const applyReorderChange = React.useCallback((transform) => {
1147
+ setReorderDraft((prev) => {
1148
+ const next = transform(prev);
1149
+ if (isSameReorder(prev, next))
1150
+ return prev;
1151
+ pushUndoSnapshot(prev);
1152
+ return next;
1153
+ });
1154
+ }, [pushUndoSnapshot]);
1155
+ function moveReorderItem(index, direction) {
1156
+ const targetIndex = index + direction;
1157
+ if (targetIndex < 0 || targetIndex >= reorderDraft.length)
1158
+ return;
1159
+ applyReorderChange((prev) => {
1160
+ const next = [...prev];
1161
+ const [item] = next.splice(index, 1);
1162
+ next.splice(targetIndex, 0, item);
1163
+ return normalizeOrder(next);
1164
+ });
1165
+ }
1166
+ function updateReorderItemOrder(index, nextOrder) {
1167
+ applyReorderChange((prev) => {
1168
+ const clamped = Math.min(Math.max(1, nextOrder), prev.length);
1169
+ const sorted = [...prev];
1170
+ const [item] = sorted.splice(index, 1);
1171
+ sorted.splice(clamped - 1, 0, item);
1172
+ return normalizeOrder(sorted);
1173
+ });
1174
+ }
1175
+ function moveItemToIndex(fromIndex, toIndex) {
1176
+ if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0)
1177
+ return;
1178
+ applyReorderChange((prev) => {
1179
+ if (fromIndex >= prev.length || toIndex >= prev.length)
1180
+ return prev;
1181
+ const next = [...prev];
1182
+ const [item] = next.splice(fromIndex, 1);
1183
+ next.splice(toIndex, 0, item);
1184
+ return normalizeOrder(next);
1185
+ });
1186
+ }
1187
+ function handleReorderDrop(targetIndex) {
1188
+ if (dragIndex !== null)
1189
+ moveItemToIndex(dragIndex, targetIndex);
1190
+ setDragIndex(null);
1191
+ }
1192
+ function reindexDraft() {
1193
+ applyReorderChange((prev) => normalizeOrder(prev));
1194
+ }
1195
+ function resetToServerOrder() {
1196
+ applyReorderChange(() => cloneReorderItems(reorderServerSnapshot));
1197
+ }
1198
+ function undoReorderChange() {
1199
+ if (reorderUndoStack.length === 0)
1200
+ return;
1201
+ const [latest, ...rest] = reorderUndoStack;
1202
+ setReorderUndoStack(rest);
1203
+ setReorderDraft(cloneReorderItems(latest));
1204
+ }
1205
+ const hasReorderChanges = reorderDraft.some((item) => sectionOrderMap.get(item.id) !== item.order);
1206
+ const canUndoReorderChanges = reorderUndoStack.length > 0;
1207
+ const config = {
1208
+ portal: "admin",
1209
+ title: "Homepage Sections",
1210
+ subtitle: "The blocks that make up the homepage, in order.",
1211
+ // Search intentionally absent: this endpoint does not read `q`, and the
1212
+ // repository's sieveQuery has no text-search concept at all, so the box
1213
+ // would accept typing and change nothing — audit-listing-search-capability
1214
+ // tracks it.
1215
+ emptyLabel: "No sections yet",
1216
+ queryKey: ["admin", "sections", "listing"],
1217
+ endpoint: ADMIN_ENDPOINTS.SECTIONS,
1218
+ pageSize: 50,
1219
+ initialView: "table",
1220
+ filterKeys: ["enabled"],
1221
+ defaultSort: sortBy("order", "ASC"),
1222
+ /*
1223
+ * 🛑 TWO options, and the omissions are measured rather than lazy.
1224
+ *
1225
+ * The ONLY composite index on `homepageSections` is (enabled ASC, order
1226
+ * ASC). `canFilter`/`canSort` in SIEVE_FIELDS govern whether sievejs
1227
+ * BUILDS a clause; the index governs whether Firestore SERVES it:
1228
+ *
1229
+ * • "Type A–Z" — `type` is canSort:false and sieve runs with
1230
+ * throwExceptions:false, so the clause is dropped SILENTLY. The
1231
+ * dropdown would change and the list would not (Root Cause #62).
1232
+ * • "-updatedAt" / "-createdAt" — these ARE canSort:true, so they look
1233
+ * safe. They work alone and FAILED_PRECONDITION the moment the
1234
+ * Enabled filter is also active, because no (enabled, updatedAt)
1235
+ * index exists. A sort that breaks only in combination is worse than
1236
+ * one that is absent. Add the index first if either is wanted.
1237
+ */
1238
+ sortOptions: [
1239
+ { value: sortBy("order", "ASC"), label: "Display order" },
1240
+ { value: sortBy("order"), label: "Display order (last first)" },
1241
+ ],
1242
+ mapRows: (response) => toRecordArray(response.items).map((item, index) => {
1243
+ const id = toStringValue(item.id, `section-${index}`);
1244
+ const type = toStringValue(item.type, "unknown");
1245
+ const surface = toStringValue(item.surface, "homepage");
1246
+ const enabled = enabledFlag.resolve(id, Boolean(item.enabled));
1247
+ return {
1248
+ id,
1249
+ primary: type.charAt(0).toUpperCase() + type.slice(1).replace(/-/g, " "),
1250
+ /*
1251
+ * The ID, not "Order: N • Enabled" — both of those are their own
1252
+ * columns now, and the id is the only thing that distinguishes two
1253
+ * sections of the SAME type, a state the id generator's timestamp
1254
+ * suffix makes reachable. `surface` shows only when it is not the
1255
+ * default, so a homepage row carries no redundant word.
1256
+ */
1257
+ secondary: surface === "homepage" ? id : `${id} · ${surface}`,
1258
+ status: enabled ? "Active" : "Inactive",
1259
+ // Not `new Date(...).toLocaleDateString()`: that yields "Invalid
1260
+ // Date" on a Firestore {toDate()} shape.
1261
+ updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
1262
+ enabled,
1263
+ order: typeof item.order === "number" ? item.order : 0,
1264
+ };
1265
+ }),
1266
+ getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
1267
+ /*
1268
+ * The explicit "true"/"false" test is load-bearing. Anything else yields
1269
+ * NO clause, so a hand-typed `?enabled=banana` shows every row — rather
1270
+ * than a clause that matches nothing and renders as a confident empty
1271
+ * list, which is the shape that reads as an answer.
1272
+ */
1273
+ buildFilters: (state) => state.enabled === "true" || state.enabled === "false"
1274
+ ? sieveFilter("enabled", SIEVE_OP.EQ, state.enabled === "true")
1275
+ : undefined,
1276
+ renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: SECTION_ENABLED_TABS, value: pendingFilters.enabled ?? "", onChange: (id) => setPendingFilters((prev) => ({ ...prev, enabled: id })) })),
1277
+ columns: [
1278
+ {
1279
+ key: "primary",
1280
+ header: "Section",
1281
+ render: (row) => (_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "medium", color: "primary", className: "truncate", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", family: "mono", className: "truncate", children: row.secondary })] })),
1282
+ },
1283
+ {
1284
+ key: "order",
1285
+ header: "Order",
1286
+ className: "w-20",
1287
+ render: (row) => _jsx(Span, { size: "sm", color: "muted", numeric: true, children: String(row.order) }),
1288
+ },
1289
+ {
1290
+ key: "status",
1291
+ header: "Status",
1292
+ className: "w-28",
1293
+ render: (row) => (_jsx(Badge, { variant: row.enabled ? "active" : "inactive", size: "xs", children: row.enabled ? "Enabled" : "Disabled" })),
1294
+ },
1295
+ {
1296
+ key: "updatedAt",
1297
+ header: "Updated",
1298
+ className: "w-32",
1299
+ render: (row) => _jsx(Span, { size: "sm", color: "muted", children: row.updatedAt }),
1300
+ },
1301
+ ],
1302
+ /*
1303
+ * 🛑 The toggle lives HERE and not in a column, on purpose.
1304
+ *
1305
+ * `initialView` only applies to an admin with no saved view preference,
1306
+ * and DataListingView suppresses the table entirely below `md` — so a
1307
+ * column-only control is invisible to anyone on a phone and to anyone
1308
+ * whose stored preference is the card view. `renderRowActions` renders
1309
+ * inside an already-click-stopped container in all three renderers (table
1310
+ * cell, list card, grid card), so unlike a normal column cell it needs no
1311
+ * stopPropagation wrapper of its own.
1312
+ *
1313
+ * `ariaLabel` carries the record name because a page of 28 switches that
1314
+ * all announce "Toggle" names nothing.
1315
+ */
1316
+ renderRowActions: (row) => (_jsxs(_Fragment, { children: [_jsx(Toggle, { size: "sm", checked: row.enabled, ariaLabel: row.enabled
1317
+ ? `${ACTIONS.ADMIN["disable-section"].label} ${row.primary}`
1318
+ : `${ACTIONS.ADMIN["enable-section"].label} ${row.primary}`, onChange: (next) => {
1319
+ void enabledFlag.set(row.id, next);
1320
+ } }), _jsx(QuickEditMenu, { actions: [
1321
+ {
1322
+ label: ACTIONS.ADMIN["delete-section"].label,
1323
+ destructive: true,
1324
+ onClick: () => setDeleteTargetId(row.id),
1325
+ },
1326
+ ] })] })),
1327
+ primaryAction: {
1328
+ label: "Add Section",
1329
+ onClick: ({ openCreatePanel }) => openCreatePanel(),
1330
+ },
1331
+ toolbarExtra: (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setReorderOpen(true), children: "Reorder sections" })),
1332
+ /*
1333
+ * 🛑 `key` is REQUIRED — see the comment on SectionEditorPanel. Without it,
1334
+ * clicking row B's pencil while row A's drawer is open edits B's record
1335
+ * with A's values.
1336
+ *
1337
+ * No `onRowClick`: declaring one would OVERRIDE DataListingView's default,
1338
+ * which already opens this editor. `renderEditor` alone is what makes the
1339
+ * rows clickable and injects the per-row pencil.
1340
+ */
1341
+ renderEditor: ({ editId, closePanel }) => (_jsx(SectionEditorPanel, { sectionId: editId ?? undefined,
1342
+ /*
1343
+ * From the parent's own full-set query, NOT from the mapped listing
1344
+ * rows: a `?panel=edit&id=…` deep link to a row that is filtered out
1345
+ * or on another page must still resolve, and the row shape has
1346
+ * already dropped `config`.
1347
+ */
1348
+ section: editId ? sections.find((s) => s.id === editId) : undefined, siblingSections: sections, isSectionsLoading: isLoading, onDone: closePanel }, editId ?? "create")),
1349
+ resolveEditorTitle: ({ isCreate }) => isCreate ? "Add Homepage Section" : "Edit Homepage Section",
1350
+ };
1351
+ /*
1352
+ * Passthrough mode (detail view).
1076
1353
  *
1077
- * Moving the RETURN rather than the hooks is deliberate and costs nothing:
1078
- * a hook must run on every render either way, so hoisting four declarations
1079
- * past 170 lines of dependent state would do identical work with far more
1080
- * room to reorder something by accident. `useFormShellState` claims no
1081
- * bottom bar — `useFormBottomActions` lives in `SectionForm` — so running it
1082
- * here has no side effect on the detail view below.
1354
+ * 🛑 Still below every hook. The list it guards shrank a lot when the editor
1355
+ * moved into its own component — `modalSections`, `draft`, `nav` and `form`
1356
+ * all left — but the reason the return sits here is unchanged, and the
1357
+ * remaining hooks are no less order-sensitive: a short-circuit above them
1358
+ * skips their calls in passthrough mode, so the hook ORDER differs between
1359
+ * the two render paths and React reads one hook's state out of another's
1360
+ * slot. That bug has now been fixed here twice.
1083
1361
  */
1084
1362
  if (hasChildren) {
1085
1363
  return _jsx(_Fragment, { children: children });
1086
1364
  }
1087
- return (_jsxs(_Fragment, { children: [_jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsx(Text, { size: "base", weight: "semibold", color: "primary", children: "Homepage Sections" }), _jsx(Text, { size: "sm", color: "muted", children: "Manage homepage sections and their display order" })] }), _jsx(Row, { align: "center", gap: "sm", children: _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setIsModalOpen(true), children: "Manage Sections" }) })] }), errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No sections found" })] }), _jsxs(Stack, { className: `mt-4 ${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsxs(Row, { align: "center", justify: "between", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Reorder Sections" }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: reindexDraft, disabled: reorderSections.isPending || reorderDraft.length === 0, children: "Reindex 1..N" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: undoReorderChange, disabled: reorderSections.isPending || !canUndoReorderChanges, children: "Undo unsaved" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: resetToServerOrder, disabled: reorderSections.isPending || !hasReorderChanges, children: "Reset to server" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => reorderSections.mutate(), disabled: !hasReorderChanges || reorderSections.isPending, children: reorderSections.isPending ? "Saving order..." : "Save order" })] })] }), reorderDraft.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No sections to reorder." })) : (_jsx(Stack, { gap: "sm", children: reorderDraft.map((item, index) => (_jsxs(Div, { layout: "grid", gap: "2", align: "center", className: "grid-cols-[auto_1fr_auto_auto_auto]", rounded: "md", padding: "xs", border: "default", draggable: true, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => handleReorderDrop(index), onDragEnd: () => setDragIndex(null), children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "\u2261" }), _jsxs(Text, { size: "sm", color: "primary", children: [item.type, " #", item.order] }), _jsx(Input, { type: "number", min: 1, max: reorderDraft.length, value: String(item.order), onChange: (event) => updateReorderItemOrder(index, Number(event.target.value) || 1), className: "w-24" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, -1), disabled: index === 0, children: "Up" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, 1), disabled: index === reorderDraft.length - 1, children: "Down" })] }, `reorder-${item.id}`))) }))] }), _jsx(Modal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), title: "Manage Homepage Section", size: "lg", children: _jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: modalSections, values: draft,
1088
- /*
1089
- * Each control writes through its own setter — `setMode`,
1090
- * `setSectionType`, `setOrder` — and several of them do more than
1091
- * store a value (choosing a type in create mode also clears the
1092
- * selected section). So this routes nothing generically.
1093
- */
1094
- onChange: () => undefined, onSubmit: () => {
1095
- form.clearErrors();
1096
- const parsed = homepageSectionFormSchema.safeParse(draft);
1097
- if (!parsed.success) {
1098
- applyZodIssues(parsed.error.issues, form.setFieldError);
1099
- return;
1100
- }
1101
- saveSection.mutate();
1102
- }, onValidationChange: () => form.validate(draft), schema: homepageSectionFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: saveSection.isPending
1103
- ? "Saving..."
1104
- : mode === "create"
1105
- ? "Create section"
1106
- : "Update section", cancelLabel: "Cancel", onCancel: () => setIsModalOpen(false), isLoading: saveSection.isPending })] }) })] }));
1365
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(SideDrawer, { isOpen: reorderOpen, onClose: () => setReorderOpen(false), title: "Reorder sections", footer: _jsxs(Row, { align: "center", justify: "end", gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: reindexDraft, disabled: reorderSections.isPending || reorderDraft.length === 0, children: "Reindex 1..N" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: undoReorderChange, disabled: reorderSections.isPending || !canUndoReorderChanges, children: "Undo unsaved" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: resetToServerOrder, disabled: reorderSections.isPending || !hasReorderChanges, children: "Reset to server" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => reorderSections.mutate(), disabled: !hasReorderChanges || reorderSections.isPending || isReorderTruncated, children: reorderSections.isPending ? "Saving order..." : "Save order" })] }), children: _jsxs(Stack, { gap: "sm", children: [isReorderTruncated && (_jsxs(Div, { textSize: "sm", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: ["Showing ", sections.length, " of ", total, " sections. Saving a partial set would renumber the rest, so Save order is disabled."] })), reorderDraft.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No sections to reorder." })) : (reorderDraft.map((item, index) => (_jsxs(Div, { layout: "grid", gap: "2", align: "center", className: "grid-cols-[auto_1fr_auto_auto_auto]", rounded: "md", padding: "xs", border: "default", draggable: true, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => handleReorderDrop(index), onDragEnd: () => setDragIndex(null), children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "\u2261" }), _jsxs(Text, { size: "sm", color: "primary", children: [item.type, " #", item.order] }), _jsx(Input, { type: "number", min: 1, max: reorderDraft.length, value: String(item.order), "aria-label": `Order for ${item.type}`, onChange: (event) => updateReorderItemOrder(index, Number(event.target.value) || 1), className: "w-24" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, -1), disabled: index === 0, children: "Up" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, 1), disabled: index === reorderDraft.length - 1, children: "Down" })] }, `reorder-${item.id}`))))] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: ACTIONS.ADMIN["delete-section"].confirmation?.title, message: ACTIONS.ADMIN["delete-section"].confirmation?.body, confirmText: ACTIONS.ADMIN["delete-section"].confirmation?.confirmLabel, isDeleting: deleteSection.isPending, onConfirm: () => {
1366
+ deleteSection.mutate(deleteTargetId);
1367
+ setDeleteTargetId(null);
1368
+ }, onClose: () => setDeleteTargetId(null) }))] }));
1107
1369
  }