@mohasinac/appkit 4.31.0 → 4.32.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/_internal/server/features/products/list-public.d.ts +18 -0
- package/dist/_internal/server/features/products/list-public.js +27 -9
- package/dist/client.d.ts +9 -0
- package/dist/client.js +12 -0
- package/dist/features/admin/components/AdminAdsView.js +36 -34
- package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
- package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
- package/dist/features/admin/components/AdminProductEditorView.js +78 -18
- package/dist/features/admin/components/AdminSectionsView.js +89 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
- package/dist/features/admin/components/AdminUserEditorView.js +65 -82
- package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
- package/dist/features/admin/constants/admin-page-tabs.js +45 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
- package/dist/features/admin/schemas/admin-user-form.js +96 -7
- package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
- package/dist/features/layout/PageTabs.d.ts +17 -0
- package/dist/features/layout/PageTabs.js +45 -0
- package/dist/features/products/components/GroupSettingsPanel.js +62 -16
- package/dist/features/search/action-index/tab-entries.d.ts +18 -0
- package/dist/features/search/action-index/tab-entries.js +128 -0
- package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
- package/dist/features/seller/components/SellerOrdersView.js +57 -19
- package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
- package/dist/features/seller/components/SellerReviewsView.js +54 -19
- package/dist/features/seller/constants/store-tabs.d.ts +64 -0
- package/dist/features/seller/constants/store-tabs.js +60 -0
- package/dist/features/seller/schemas/order-forms.d.ts +33 -0
- package/dist/features/seller/schemas/order-forms.js +75 -0
- package/dist/features/seller/schemas/review-forms.d.ts +37 -0
- package/dist/features/seller/schemas/review-forms.js +82 -0
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +5 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
- package/dist/features/shipments/schemas/validation.d.ts +41 -0
- package/dist/features/shipments/schemas/validation.js +49 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +67 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/next/index.d.ts +8 -0
- package/dist/next/index.js +37 -0
- package/dist/react/hooks/useTabParam.d.ts +19 -0
- package/dist/react/hooks/useTabParam.js +65 -0
- package/dist/schemas/registry.d.ts +27 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
// audit-form-mutation-hook-multi-ok: saveMutation is the single combined save
|
|
4
|
-
// across all tabs; deleteMutation is a separate destructive action, not a
|
|
5
|
-
// second per-tab save.
|
|
6
|
-
import { Row } from "@mohasinac/appkit/ui";
|
|
7
3
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
8
4
|
import React from "react";
|
|
9
5
|
import { z } from "zod";
|
|
10
6
|
import { useQuery } from "@tanstack/react-query";
|
|
11
|
-
import { Button, Card, CardBody, ConfirmDeleteModal, Div, PaginatedSelect,
|
|
7
|
+
import { Button, Card, CardBody, ConfirmDeleteModal, Div, PaginatedSelect, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, Show, StickyToolbar, } from "../../../ui";
|
|
12
8
|
import { useFormShellState, applyZodIssues, FormErrorSummary, FormShellContext } from "../../../ui/forms";
|
|
9
|
+
import { SectionForm, useSectionFormNav } from "../../shell";
|
|
13
10
|
// Admin form-input validation — deliberately a small local schema, not the
|
|
14
11
|
// broader `productItemSchema` (that one requires `id`/`price`/`status` as
|
|
15
12
|
// non-optional, since it's the full read/display shape used across many
|
|
@@ -119,12 +116,6 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
|
|
|
119
116
|
const [mode, setMode] = React.useState("standard");
|
|
120
117
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
121
118
|
const { showToast } = useToast();
|
|
122
|
-
const { shellCtx, setFieldError, validate } = useFormShellState(adminProductFormSchema);
|
|
123
|
-
// Live validation — re-run on every product change, not just on submit.
|
|
124
|
-
React.useEffect(() => {
|
|
125
|
-
validate(product);
|
|
126
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
|
-
}, [product, validate]);
|
|
128
119
|
const productQuery = useQuery({
|
|
129
120
|
queryKey: ["admin", "product", productId],
|
|
130
121
|
queryFn: async () => {
|
|
@@ -189,13 +180,82 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
|
|
|
189
180
|
},
|
|
190
181
|
});
|
|
191
182
|
const isSubmitting = saveMutation.isPending || productQuery.isLoading;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
183
|
+
/*
|
|
184
|
+
* 🛑 `ProductForm` is a FIELD GROUP, not a form — its parent owns the submit,
|
|
185
|
+
* which is why it renders inside a section here rather than mounting a
|
|
186
|
+
* SectionForm of its own. `SellerProductShell` composes the same child the
|
|
187
|
+
* same way; nesting one SectionForm inside another would give the page two
|
|
188
|
+
* action rows and two mobile bars.
|
|
189
|
+
*/
|
|
190
|
+
const sections = React.useMemo(() => [
|
|
191
|
+
{
|
|
192
|
+
id: "type",
|
|
193
|
+
label: "Listing type",
|
|
194
|
+
required: true,
|
|
195
|
+
fields: ["listingType"],
|
|
196
|
+
render: () => (_jsx(Stack, { gap: "md", children: _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Listing Type" }), _jsxs(Tabs, { value: mode, onChange: handleModeChange, children: [_jsxs(TabsList, { children: [(!enabledListingTypes || enabledListingTypes.includes("standard")) && (_jsx(TabsTrigger, { value: "standard", children: "Standard" })), (!enabledListingTypes || enabledListingTypes.includes("auction")) && (_jsx(TabsTrigger, { value: "auction", children: "Auction" })), (!enabledListingTypes || enabledListingTypes.includes("pre-order")) && (_jsx(TabsTrigger, { value: "preorder", children: "Pre-order" })), (!enabledListingTypes || enabledListingTypes.includes("prize-draw")) && (_jsx(TabsTrigger, { value: "prize-draw", children: "Prize Draw" })), (!enabledListingTypes || enabledListingTypes.includes("classified")) && (_jsx(TabsTrigger, { value: "classified", children: "Classified" })), (!enabledListingTypes || enabledListingTypes.includes("digital-code")) && (_jsx(TabsTrigger, { value: "digital-code", children: "Digital Code" })), (!enabledListingTypes || enabledListingTypes.includes("live")) && (_jsx(TabsTrigger, { value: "live", children: "Live Item" }))] }), _jsx(TabsContent, { value: "standard" }), _jsx(TabsContent, { value: "auction" }), _jsx(TabsContent, { value: "preorder" }), _jsx(TabsContent, { value: "prize-draw" }), _jsx(TabsContent, { value: "classified" }), _jsx(TabsContent, { value: "digital-code" }), _jsx(TabsContent, { value: "live" })] })] }) })),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "classification",
|
|
200
|
+
label: "Details",
|
|
201
|
+
required: true,
|
|
202
|
+
fields: ["title", "price", "storeId", "categorySlug", "brandSlug"],
|
|
203
|
+
render: () => (_jsx(Stack, { gap: "md", children: _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Classification" }), _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: "Store" }), _jsx(PaginatedSelect, { value: product.storeId ?? null, onChange: handleStoreSelect, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] }), _jsx(ProductForm, { product: product, onChange: setProduct, renderGroupSettings: isEdit && productId
|
|
204
|
+
? (p) => (_jsx(GroupSettingsPanel, { productId: productId, productSlug: p.slug ?? productId, groupId: p.groupId, isGroupParent: p.isGroupParent, groupParentSlug: p.groupParentSlug, groupChildSlugs: p.groupChildSlugs, groupTitle: p.groupTitle, isAuction: modeFromProduct(p) === "auction", storeProductsEndpoint: ADMIN_ENDPOINTS.PRODUCTS, onGroupChanged: () => productQuery.refetch() }))
|
|
205
|
+
: undefined, renderGroupJoinField: ({ label, value, onChange, disabled }) => (_jsx(GroupInlineSelect, { scope: "admin", value: value ?? "", onChange: (id) => onChange(id || undefined), disabled: disabled, label: label, placeholder: "None (standalone product)", allowCreate: false })), renderCategorySelector: ({ label, value, onChange, disabled }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: label }), _jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: label, disabled: disabled, createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })), renderBrandSelector: (args) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: args.label }), _jsx(PaginatedSelect, { value: args.multi ? null : (args.value || null), onChange: (v) => args.onValueChange(v ?? ""), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type brand name\u2026", noResultsText: "No brands found", ariaLabel: args.label, disabled: args.disabled, createLabel: "Brand", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })) })] })] }) })),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "inventory",
|
|
209
|
+
label: "Inventory",
|
|
210
|
+
fields: ["barcodeId"],
|
|
211
|
+
render: () => (_jsx(Stack, { gap: "md", children: _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Inventory" }), _jsx(BarcodeField, { value: product.barcodeId ?? "", onChange: (v) => setProduct((prev) => ({ ...prev, barcodeId: v || undefined })), onScan: (v) => setProduct((prev) => ({ ...prev, barcodeId: v })), helperText: "Leave blank to auto-generate. Scan a pre-printed sticker to link it." })] }) })),
|
|
212
|
+
},
|
|
213
|
+
], [product, mode, enabledListingTypes, isEdit, productId]);
|
|
214
|
+
const nav = useSectionFormNav(sections, product, { scope: "admin:product-editor" });
|
|
215
|
+
/*
|
|
216
|
+
* Declared AFTER the sections, so the error summary's jump links and the
|
|
217
|
+
* per-section error badges have a field map to resolve against. Wired the
|
|
218
|
+
* other way round it still validates and still lists the errors — it just
|
|
219
|
+
* cannot say which panel each one is in, which is the whole point of the
|
|
220
|
+
* summary on a form this long.
|
|
221
|
+
*/
|
|
222
|
+
const { shellCtx, setFieldError, validate } = useFormShellState(adminProductFormSchema, {
|
|
223
|
+
sections: nav.sectionMeta,
|
|
224
|
+
onGoToSection: nav.goToSection,
|
|
225
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
226
|
+
});
|
|
227
|
+
// Live validation — re-run on every product change, not just on submit.
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
validate(product);
|
|
230
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
231
|
+
}, [product, validate]);
|
|
232
|
+
/*
|
|
233
|
+
* ONE submit path. The sidebar button used `form="product-editor-form"` to
|
|
234
|
+
* reach across the two-panel layout into the form element; with no form
|
|
235
|
+
* element to associate with, both callers call this instead.
|
|
236
|
+
*/
|
|
237
|
+
const handleSubmit = React.useCallback(() => {
|
|
238
|
+
const parsed = adminProductFormSchema.safeParse(product);
|
|
239
|
+
if (!parsed.success) {
|
|
240
|
+
applyZodIssues(parsed.error.issues, setFieldError);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
saveMutation.mutate();
|
|
244
|
+
}, [product, saveMutation, setFieldError]);
|
|
245
|
+
const actionSidebar = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Card, { variant: "outlined", padding: "md", spacing: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: isEdit ? product.status ?? "—" : "New" }), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "button", className: "w-full", onClick: handleSubmit, isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete product" }))] }) }));
|
|
246
|
+
const formContent = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(SectionForm, { sections: sections, values: product,
|
|
247
|
+
/*
|
|
248
|
+
* The sections write through `setProduct` directly - `ProductForm` and
|
|
249
|
+
* the type tabs each own their own state updates - so this exists only
|
|
250
|
+
* for anything a section chooses to route generically.
|
|
251
|
+
*/
|
|
252
|
+
onChange: (partial) => setProduct((prev) => ({ ...prev, ...partial })), onSubmit: handleSubmit, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: isEdit ? "Save changes" : "Create product", isLoading: isSubmitting, destructiveAction: isEdit
|
|
253
|
+
? {
|
|
254
|
+
label: "Delete product",
|
|
255
|
+
onClick: () => setDeleteOpen(true),
|
|
256
|
+
disabled: deleteMutation.isPending,
|
|
257
|
+
}
|
|
258
|
+
: undefined }) }));
|
|
199
259
|
if (embedded) {
|
|
200
260
|
return _jsx(Div, { className: `${__O.yAuto} ${__P.p4}`, children: formContent });
|
|
201
261
|
}
|
|
@@ -6,9 +6,11 @@ import React from "react";
|
|
|
6
6
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
7
|
import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
|
|
8
8
|
import { CategoryInlineSelect } from "../../seller/components/CategoryInlineSelect";
|
|
9
|
-
import { Button, Checkbox, Div,
|
|
9
|
+
import { Button, Checkbox, Div, Input, Modal, Row, Select, Stack, Text, Textarea, useToast } from "../../../ui";
|
|
10
10
|
import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
|
|
11
|
-
import { homepageSectionFormSchema } from "../schemas/homepage-section-form";
|
|
11
|
+
import { homepageSectionFormSchema, } from "../schemas/homepage-section-form";
|
|
12
|
+
import { SectionForm, useSectionFormNav } from "../../shell";
|
|
13
|
+
import { useFormShellState, FormShellContext } from "../../../ui/forms";
|
|
12
14
|
import { apiClient } from "../../../http";
|
|
13
15
|
import { ADMIN_ENDPOINTS } from "../../../constants";
|
|
14
16
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
@@ -952,34 +954,89 @@ export function AdminSectionsView({ children }) {
|
|
|
952
954
|
}));
|
|
953
955
|
const hasReorderChanges = reorderDraft.some((item) => sectionOrderMap.get(item.id) !== item.order);
|
|
954
956
|
const canUndoReorderChanges = reorderUndoStack.length > 0;
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
957
|
+
/**
|
|
958
|
+
* The modal's fields, as three hand-written sections.
|
|
959
|
+
*
|
|
960
|
+
* The envelope (type / order / enabled) could derive from
|
|
961
|
+
* `homepageSectionFormSchema`, but the builder cannot: it is 22 discriminated
|
|
962
|
+
* shapes rendered by `renderTypedBuilder()`, and `configJson` is annotated
|
|
963
|
+
* `kind: "list"`, which has no control. So all three are hand-written and the
|
|
964
|
+
* schema stays what VALIDATES rather than what renders — the
|
|
965
|
+
* `AdminCategoryEditorView` arrangement.
|
|
966
|
+
*
|
|
967
|
+
* The builder section is `when`-gated on the chosen type having one, which is
|
|
968
|
+
* what makes it disappear for a raw-JSON section type instead of rendering an
|
|
969
|
+
* empty panel.
|
|
970
|
+
*/
|
|
971
|
+
const modalSections = React.useMemo(() => [
|
|
972
|
+
{
|
|
973
|
+
id: "basics",
|
|
974
|
+
label: "Section",
|
|
975
|
+
required: true,
|
|
976
|
+
fields: ["sectionType", "order", "enabled"],
|
|
977
|
+
render: () => (_jsxs(Stack, { gap: "md", children: [_jsx(Select, { label: "Mode", value: mode, onValueChange: (value) => setMode(value), options: [
|
|
978
|
+
{ label: "Create new section", value: "create" },
|
|
979
|
+
{ label: "Edit existing section", value: "edit" },
|
|
980
|
+
] }), mode === "edit" ? (_jsx(Select, { label: "Section", value: selectedSectionId, onValueChange: setSelectedSectionId, placeholder: "Select section", options: sections.map((section) => ({
|
|
981
|
+
label: `${section.type} (#${section.order})`,
|
|
982
|
+
value: section.id,
|
|
983
|
+
})) })) : null, _jsx(Select, { label: "Section type", value: sectionType, onValueChange: (value) => {
|
|
984
|
+
setSectionType(value);
|
|
985
|
+
if (mode === "create") {
|
|
986
|
+
setSelectedSectionId("");
|
|
987
|
+
}
|
|
988
|
+
}, options: SECTION_TYPE_OPTIONS.map((type) => ({
|
|
989
|
+
label: type,
|
|
990
|
+
value: type,
|
|
991
|
+
})), disabled: mode === "edit" }), _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: [
|
|
992
|
+
{ label: "Enabled", value: "true" },
|
|
993
|
+
{ label: "Disabled", value: "false" },
|
|
994
|
+
] })] })),
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
id: "builder",
|
|
998
|
+
label: "Content",
|
|
999
|
+
when: () => isTypedBuilder,
|
|
1000
|
+
render: () => _jsx(_Fragment, { children: renderTypedBuilder() }),
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
id: "config",
|
|
1004
|
+
label: isTypedBuilder ? "Generated config" : "Section config (JSON)",
|
|
1005
|
+
fields: ["configJson"],
|
|
1006
|
+
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
|
|
1007
|
+
? "This JSON is generated from typed controls above."
|
|
1008
|
+
: "Provide section config JSON for this section type." })),
|
|
1009
|
+
},
|
|
1010
|
+
], [
|
|
1011
|
+
mode, selectedSectionId, sections, sectionType, order, enabled,
|
|
1012
|
+
isTypedBuilder, configJson, renderTypedBuilder,
|
|
1013
|
+
]);
|
|
1014
|
+
/** What the nav and the error summary key on. */
|
|
1015
|
+
const draft = React.useMemo(() => ({ sectionType, order, enabled, configJson }), [sectionType, order, enabled, configJson]);
|
|
1016
|
+
const nav = useSectionFormNav(modalSections, draft, { scope: "admin:homepage-sections" });
|
|
1017
|
+
const form = useFormShellState(homepageSectionFormSchema, {
|
|
1018
|
+
sections: nav.sectionMeta,
|
|
1019
|
+
onGoToSection: nav.goToSection,
|
|
1020
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
1021
|
+
});
|
|
1022
|
+
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,
|
|
1023
|
+
/*
|
|
1024
|
+
* Each control writes through its own setter — `setMode`,
|
|
1025
|
+
* `setSectionType`, `setOrder` — and several of them do more than
|
|
1026
|
+
* store a value (choosing a type in create mode also clears the
|
|
1027
|
+
* selected section). So this routes nothing generically.
|
|
1028
|
+
*/
|
|
1029
|
+
onChange: () => undefined, onSubmit: () => {
|
|
1030
|
+
form.clearErrors();
|
|
1031
|
+
const parsed = homepageSectionFormSchema.safeParse(draft);
|
|
1032
|
+
if (!parsed.success) {
|
|
1033
|
+
applyZodIssues(parsed.error.issues, form.setFieldError);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
saveSection.mutate();
|
|
1037
|
+
}, onValidationChange: () => form.validate(draft), schema: homepageSectionFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: saveSection.isPending
|
|
1038
|
+
? "Saving..."
|
|
1039
|
+
: mode === "create"
|
|
1040
|
+
? "Create section"
|
|
1041
|
+
: "Update section", cancelLabel: "Cancel", onCancel: () => setIsModalOpen(false), isLoading: saveSection.isPending })] }) })] }));
|
|
985
1042
|
}
|