@mohasinac/appkit 4.19.0 โ 4.21.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/reviews/actions.js +4 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +3 -3
- package/dist/_internal/shared/features/reviews/schema.js +11 -1
- package/dist/features/seller/components/SellerProductShell.js +107 -71
- package/dist/features/seller/components/SellerShippingView.js +21 -18
- package/dist/features/seller/components/SellerStorefrontView.js +21 -19
- package/dist/features/shell/FormShell.d.ts +4 -4
- package/dist/features/shell/FormShell.js +1 -1
- package/dist/features/shell/index.d.ts +0 -2
- package/dist/features/shell/index.js +0 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +24 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -3
- package/dist/schemas/api-routes.d.ts +9 -0
- package/dist/schemas/api-routes.js +5 -3
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ export async function createReviewAction(input) {
|
|
|
13
13
|
if (!parsed.success) {
|
|
14
14
|
throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
|
|
15
15
|
}
|
|
16
|
-
const { productId, storeId, rating, title,
|
|
16
|
+
const { productId, storeId, rating, title, comment, images } = parsed.data;
|
|
17
17
|
await assertNotDuplicateReview(productId, user.uid);
|
|
18
18
|
const product = await productRepository.findById(productId);
|
|
19
19
|
const profile = await userRepository.findById(user.uid);
|
|
@@ -27,7 +27,9 @@ export async function createReviewAction(input) {
|
|
|
27
27
|
userAvatar: profile?.photoURL ?? "",
|
|
28
28
|
rating,
|
|
29
29
|
title,
|
|
30
|
-
comment: body
|
|
30
|
+
// Was `comment: body` โ the schema called the field `body` and this
|
|
31
|
+
// line translated it. Both now use the document's own name.
|
|
32
|
+
comment,
|
|
31
33
|
images: images ?? [],
|
|
32
34
|
verified: false,
|
|
33
35
|
status: "pending",
|
|
@@ -4,23 +4,23 @@ export declare const createReviewSchema: z.ZodObject<{
|
|
|
4
4
|
storeId: z.ZodString;
|
|
5
5
|
rating: z.ZodNumber;
|
|
6
6
|
title: z.ZodString;
|
|
7
|
-
|
|
7
|
+
comment: z.ZodString;
|
|
8
8
|
images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
9
9
|
orderId: z.ZodOptional<z.ZodString>;
|
|
10
10
|
}, "strip", z.ZodTypeAny, {
|
|
11
11
|
title: string;
|
|
12
|
-
body: string;
|
|
13
12
|
storeId: string;
|
|
14
13
|
rating: number;
|
|
15
14
|
images: string[];
|
|
16
15
|
productId: string;
|
|
16
|
+
comment: string;
|
|
17
17
|
orderId?: string | undefined;
|
|
18
18
|
}, {
|
|
19
19
|
title: string;
|
|
20
|
-
body: string;
|
|
21
20
|
storeId: string;
|
|
22
21
|
rating: number;
|
|
23
22
|
productId: string;
|
|
23
|
+
comment: string;
|
|
24
24
|
orderId?: string | undefined;
|
|
25
25
|
images?: string[] | undefined;
|
|
26
26
|
}>;
|
|
@@ -5,7 +5,17 @@ export const createReviewSchema = z.object({
|
|
|
5
5
|
storeId: z.string().min(1),
|
|
6
6
|
rating: z.number().int().min(REVIEW_MIN_RATING).max(REVIEW_MAX_RATING),
|
|
7
7
|
title: z.string().min(1).max(REVIEW_TITLE_MAX_LENGTH),
|
|
8
|
-
|
|
8
|
+
/*
|
|
9
|
+
* ๐ This was named `body`. The field is `comment` โ on `ReviewDocument`
|
|
10
|
+
* (schemas/firestore.ts:39), on `CreateReviewActionInput`, and in what
|
|
11
|
+
* `useCreateReview` actually sends. So this schema disagreed with the
|
|
12
|
+
* document, the action AND the client, and would have rejected every real
|
|
13
|
+
* submission the moment anything used it to validate one.
|
|
14
|
+
*
|
|
15
|
+
* It went unnoticed because `POST /api/reviews` passed `body as any` and
|
|
16
|
+
* never consulted it.
|
|
17
|
+
*/
|
|
18
|
+
comment: z.string().min(REVIEW_BODY_MIN_LENGTH).max(REVIEW_BODY_MAX_LENGTH),
|
|
9
19
|
images: z.array(z.string().url()).max(REVIEW_IMAGES_MAX).default([]),
|
|
10
20
|
orderId: z.string().optional(),
|
|
11
21
|
});
|
|
@@ -3,9 +3,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { FormShell,
|
|
6
|
+
import { FormShell, SectionForm, useSectionFormNav, useFormShell } from "../../shell";
|
|
7
7
|
import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary } from "../../../ui/forms";
|
|
8
|
-
import { Alert, Button, Div, FormField, FormGroup, Heading, Section, Stack, TagInput, Text, Toggle, useToast } from "../../../ui";
|
|
8
|
+
import { Alert, Button, Div, FormField, FormGroup, Heading, Section, Stack, TagInput, Text, Toggle, useToast, Row } from "../../../ui";
|
|
9
9
|
import { ImageUpload, MediaUploadField, MediaUploadList, useMediaUpload } from "../../media";
|
|
10
10
|
import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddressSelectorCreate";
|
|
11
11
|
import { QuickProductForm } from "./QuickProductForm";
|
|
@@ -153,25 +153,65 @@ function StepLiveItemSettings({ values, onChange, }) {
|
|
|
153
153
|
function StepPrintMetaSettings({ values, onChange, intro, }) {
|
|
154
154
|
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: intro }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "printSize", label: "Size", type: "text", value: values.printSize ?? "", onChange: (v) => onChange({ printSize: v }), placeholder: "e.g. A4, 12x18 in" }), _jsx(FormField, { name: "printMaterial", label: "Material", type: "text", value: values.printMaterial ?? "", onChange: (v) => onChange({ printMaterial: v }), placeholder: "e.g. Matte photo paper, Vinyl" })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "printFinish", label: "Finish (optional)", type: "text", value: values.printFinish ?? "", onChange: (v) => onChange({ printFinish: v }), placeholder: "e.g. Glossy, Matte, Holographic" }), _jsx(FormField, { name: "printEditionSize", label: "Edition Size (optional)", type: "number", value: String(values.printEditionSize ?? ""), onChange: (v) => onChange({ printEditionSize: v ? Number(v) : undefined }), placeholder: "Leave blank for an open edition" })] })] }));
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
// โโ Type-specific step registry โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
157
|
+
// Replaces the ad-hoc per-type ternary chain that used to pick the wizard step
|
|
158
|
+
// (create mode) / section (edit mode) โ one Record lookup instead of a
|
|
159
|
+
// duplicated 5(+)-way chain in each mode.
|
|
160
|
+
/*
|
|
161
|
+
* Per-section schemas for the product form.
|
|
162
|
+
*
|
|
163
|
+
* These replace the `validate: (v) => "message" | null` callbacks each step
|
|
164
|
+
* used to carry. The rules are unchanged; what changes is where the failure
|
|
165
|
+
* surfaces. A step-level string could only ever render as one banner above the
|
|
166
|
+
* whole step โ a Zod issue carries a PATH, so the message lands on the field
|
|
167
|
+
* that caused it and `<FormErrorSummary>` can offer a jump link to it.
|
|
168
|
+
*
|
|
169
|
+
* Kept beside the sections rather than folded into `sellerProductSchema`
|
|
170
|
+
* because they are per-LISTING-TYPE: a starting bid is required for an auction
|
|
171
|
+
* and meaningless for a sticker sheet, and a single whole-form schema would
|
|
172
|
+
* have to branch on type for every one of them.
|
|
173
|
+
*/
|
|
174
|
+
const sectionSchemas = {
|
|
175
|
+
basic: z.object({
|
|
176
|
+
title: z.string().trim().min(3, "Title must be at least 3 characters"),
|
|
177
|
+
description: z.string().trim().min(20, "Description must be at least 20 characters"),
|
|
178
|
+
}).passthrough(),
|
|
179
|
+
media: z.object({
|
|
180
|
+
mainImage: z.string().min(1, "A main image is required"),
|
|
181
|
+
}).passthrough(),
|
|
182
|
+
auction: z.object({
|
|
183
|
+
startingBid: z.coerce.number({ invalid_type_error: "Starting bid is required" }).positive("Starting bid must be greater than zero"),
|
|
184
|
+
auctionEndDate: z.string().min(1, "Auction end date is required"),
|
|
185
|
+
}).passthrough(),
|
|
186
|
+
preorder: z.object({
|
|
187
|
+
preOrderDeliveryDate: z.string().min(1, "Estimated delivery date is required"),
|
|
188
|
+
}).passthrough(),
|
|
189
|
+
classified: z.object({
|
|
190
|
+
classifiedCity: z.string().trim().min(1, "City is required for classified listings"),
|
|
191
|
+
}).passthrough(),
|
|
192
|
+
live: z.object({
|
|
193
|
+
liveSpecies: z.string().trim().min(1, "Species name is required"),
|
|
194
|
+
}).passthrough(),
|
|
195
|
+
};
|
|
196
|
+
const TYPE_SPECIFIC_SECTIONS = {
|
|
157
197
|
auction: {
|
|
158
198
|
id: "auction",
|
|
159
199
|
label: "Auction Settings",
|
|
160
200
|
render: ({ values, onChange }) => (_jsx(StepAuctionSettings, { values: values, onChange: onChange })),
|
|
161
|
-
|
|
201
|
+
schema: sectionSchemas.auction,
|
|
162
202
|
},
|
|
163
203
|
"pre-order": {
|
|
164
204
|
id: "preorder",
|
|
165
205
|
label: "Pre-Order",
|
|
166
206
|
sectionHeading: "Pre-Order Settings",
|
|
167
207
|
render: ({ values, onChange }) => (_jsx(StepPreOrderSettings, { values: values, onChange: onChange })),
|
|
168
|
-
|
|
208
|
+
schema: sectionSchemas.preorder,
|
|
169
209
|
},
|
|
170
210
|
classified: {
|
|
171
211
|
id: "classified",
|
|
172
212
|
label: "Meetup Details",
|
|
173
213
|
render: ({ values, onChange }) => (_jsx(StepClassifiedSettings, { values: values, onChange: onChange })),
|
|
174
|
-
|
|
214
|
+
schema: sectionSchemas.classified,
|
|
175
215
|
},
|
|
176
216
|
"digital-code": {
|
|
177
217
|
id: "digitalcode",
|
|
@@ -182,7 +222,7 @@ const TYPE_SPECIFIC_STEPS = {
|
|
|
182
222
|
id: "live",
|
|
183
223
|
label: "Live Item Details",
|
|
184
224
|
render: ({ values, onChange }) => (_jsx(StepLiveItemSettings, { values: values, onChange: onChange })),
|
|
185
|
-
|
|
225
|
+
schema: sectionSchemas.live,
|
|
186
226
|
},
|
|
187
227
|
art: {
|
|
188
228
|
id: "printmeta",
|
|
@@ -241,13 +281,18 @@ const EDIT_SECTIONS = [
|
|
|
241
281
|
export function SellerProductShell({ mode, listingType = "standard", initialValues, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, }) {
|
|
242
282
|
const [draft, setDraft] = useState(initialValues ?? { status: "draft", condition: "new" });
|
|
243
283
|
const [formMode, setFormMode] = useState(mode === "create" && listingType === "standard" ? "quick" : "full");
|
|
244
|
-
|
|
284
|
+
/*
|
|
285
|
+
* Declared here, ABOVE the sections array, because section render callbacks
|
|
286
|
+
* close over `setFieldError`/`clearErrors`. The nav seam cannot be supplied
|
|
287
|
+
* yet โ it needs the sections โ so it is composed in below. That ordering is
|
|
288
|
+
* why the original `wizardShellCtx` existed; only its contents change.
|
|
289
|
+
*/
|
|
290
|
+
const { shellCtx: baseShellCtx, setFieldError, clearErrors, validate } = useFormShellState(sellerProductSchema);
|
|
245
291
|
const [stepError, setStepError] = useState(null);
|
|
246
292
|
const { isDirty, markDirty, markClean } = useFormShell();
|
|
247
293
|
const router = useRouter();
|
|
248
294
|
const { showToast } = useToast();
|
|
249
295
|
const { upload: shellUpload } = useMediaUpload();
|
|
250
|
-
const { shellCtx, setFieldError, clearErrors, validate } = useFormShellState(sellerProductSchema);
|
|
251
296
|
const update = useCallback((partial) => {
|
|
252
297
|
setDraft((prev) => {
|
|
253
298
|
const next = { ...prev, ...partial };
|
|
@@ -346,63 +391,73 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
346
391
|
}
|
|
347
392
|
}, [draft, onPublish, markClean, clearErrors, setFieldError, showToast]);
|
|
348
393
|
const listingTypeLabel = pluginForMode(listingType).typeLabel;
|
|
349
|
-
const
|
|
350
|
-
const
|
|
394
|
+
const typeSpecificSection = TYPE_SPECIFIC_SECTIONS[listingType] ?? null;
|
|
395
|
+
const sections = [
|
|
351
396
|
{
|
|
397
|
+
id: "basic",
|
|
352
398
|
label: "Basic",
|
|
399
|
+
required: true,
|
|
400
|
+
quick: true,
|
|
353
401
|
fields: ["title", "description", "category", "brand", "condition", "tags", "barcodeId"],
|
|
354
402
|
render: ({ values, onChange }) => (_jsx(StepBasic, { values: values, onChange: onChange, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })),
|
|
355
|
-
|
|
356
|
-
if (!v.title?.trim() || v.title.trim().length < 3)
|
|
357
|
-
return "Title must be at least 3 characters";
|
|
358
|
-
if (!v.description?.trim() || v.description.trim().length < 20)
|
|
359
|
-
return "Description must be at least 20 characters";
|
|
360
|
-
return null;
|
|
361
|
-
},
|
|
403
|
+
schema: sectionSchemas.basic,
|
|
362
404
|
},
|
|
363
405
|
{
|
|
406
|
+
id: "media",
|
|
364
407
|
label: "Media",
|
|
408
|
+
keepMounted: true,
|
|
365
409
|
fields: ["mainImage", "images", "video", "video.url", "video.duration", "video.thumbnailUrl", "youtubeId", "externalVideoUrl"],
|
|
366
410
|
render: ({ values, onChange }) => (_jsx(StepMedia, { values: values, onChange: onChange, storeSlug: storeSlug })),
|
|
367
|
-
|
|
411
|
+
schema: sectionSchemas.media,
|
|
368
412
|
},
|
|
369
|
-
...(
|
|
413
|
+
...(typeSpecificSection ? [typeSpecificSection] : []),
|
|
370
414
|
{
|
|
415
|
+
id: "pricing",
|
|
371
416
|
label: "Pricing",
|
|
417
|
+
required: true,
|
|
418
|
+
quick: true,
|
|
372
419
|
fields: ["price", "compareAtPrice", "stockQuantity", "allowOffers", "minOfferPercent", "gstRate", "hsnCode"],
|
|
373
420
|
render: ({ values, onChange }) => (_jsx(StepPricing, { values: values, onChange: onChange, listingType: listingType })),
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
421
|
+
// Built per render because whether stock is required depends on the
|
|
422
|
+
// listing type's own plugin โ a digital code has no stock to speak of.
|
|
423
|
+
schema: z
|
|
424
|
+
.object({
|
|
425
|
+
price: z.coerce
|
|
426
|
+
.number({ invalid_type_error: "Price is required" })
|
|
427
|
+
.positive("Price is required"),
|
|
428
|
+
stockQuantity: pluginForMode(listingType).showsStockQuantity
|
|
429
|
+
? z.coerce.number({ invalid_type_error: "Stock quantity is required" }).int().min(0)
|
|
430
|
+
: z.coerce.number().int().min(0).optional(),
|
|
431
|
+
})
|
|
432
|
+
.passthrough(),
|
|
383
433
|
},
|
|
384
434
|
{
|
|
435
|
+
id: "shipping",
|
|
385
436
|
label: "Shipping",
|
|
386
437
|
fields: ["shippingPaidBy", "pickupAddressId", "insurance", "insuranceCost"],
|
|
387
438
|
render: ({ values, onChange }) => (_jsx(StepShipping, { values: values, onChange: onChange, renderAddressSelector: renderAddressSelector })),
|
|
388
439
|
},
|
|
389
440
|
{
|
|
441
|
+
id: "publish",
|
|
390
442
|
label: "Publish",
|
|
391
443
|
fields: ["seoTitle", "seoDescription", "isOnSale", "status"],
|
|
392
444
|
render: ({ values, onChange }) => (_jsx(StepPublish, { values: values, onChange: onChange })),
|
|
393
445
|
},
|
|
394
446
|
];
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
447
|
+
/*
|
|
448
|
+
* One nav for BOTH modes. Create used to be a wizard with its own
|
|
449
|
+
* fieldToStepIndex map and a separately-composed wizardShellCtx, while edit
|
|
450
|
+
* rendered the same sections as a single page โ two layouts over one set of
|
|
451
|
+
* content, kept in step by hand.
|
|
452
|
+
*/
|
|
453
|
+
const { openIds, setOpenIds, goToSection, fieldToSectionIndex, sectionMeta } = useSectionFormNav(sections, draft);
|
|
454
|
+
/** The one context both modes render through. */
|
|
455
|
+
const shellCtx = useMemo(() => ({
|
|
456
|
+
...baseShellCtx,
|
|
457
|
+
sections: sectionMeta,
|
|
458
|
+
onGoToSection: goToSection,
|
|
459
|
+
fieldToSectionIndex,
|
|
460
|
+
}), [baseShellCtx, sectionMeta, goToSection, fieldToSectionIndex]);
|
|
406
461
|
// Live validation โ re-run on every draft change, not just on submit, so
|
|
407
462
|
// FormErrorSummary and inline field errors both stay current as the
|
|
408
463
|
// seller types (per the "even on-change errors must be shown" requirement).
|
|
@@ -410,34 +465,15 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
410
465
|
validate(draft);
|
|
411
466
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
412
467
|
}, [draft, validate]);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
setStepError(null);
|
|
424
|
-
if (currentStep < steps.length - 1) {
|
|
425
|
-
setCurrentStep((c) => c + 1);
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
try {
|
|
429
|
-
await handlePublish();
|
|
430
|
-
}
|
|
431
|
-
catch (err) {
|
|
432
|
-
void normalizeError(err);
|
|
433
|
-
showToast(err instanceof Error ? err.message : "Something went wrong.", "error");
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}, [currentStep, steps, draft, handlePublish, showToast]);
|
|
437
|
-
// Step error badges โ run each step's validate against current draft
|
|
438
|
-
const stepValidationErrors = useMemo(() => steps.map((s) => (s.validate ? Boolean(s.validate(draft)) : false)),
|
|
439
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
440
|
-
[draft]);
|
|
468
|
+
/*
|
|
469
|
+
* `handleNext` and `stepValidationErrors` are gone with the wizard.
|
|
470
|
+
*
|
|
471
|
+
* The next-gate was the thing SectionForm exists to remove: it ran the
|
|
472
|
+
* current step's `validate` and REFUSED to advance, so a seller who could
|
|
473
|
+
* not satisfy step 2 could not reach step 5 to see what else was needed.
|
|
474
|
+
* Every section is now reachable at any time and the shared submit is what
|
|
475
|
+
* checks the whole draft.
|
|
476
|
+
*/
|
|
441
477
|
const breadcrumb = mode === "create" ? `Store / ${listingTypeLabel}s / New` : `Store / ${listingTypeLabel}s / Edit`;
|
|
442
478
|
const title = mode === "create"
|
|
443
479
|
? `New ${listingTypeLabel}`
|
|
@@ -452,12 +488,12 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
452
488
|
}) }) }) }));
|
|
453
489
|
}
|
|
454
490
|
if (mode === "create") {
|
|
455
|
-
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, splitPreview: !!previewSlot, renderBottomBar: () => (_jsxs(Div, { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(
|
|
491
|
+
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, splitPreview: !!previewSlot, renderBottomBar: () => (_jsxs(Div, { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(Div, { paddingX: "x-5", padding: "y-sm", children: _jsx(FormErrorSummary, {}) }) }), _jsx(Div, { paddingX: "x-5", padding: "b-sm", children: _jsxs(Row, { gap: "sm", justify: "between", wrap: true, children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => void handleSave(), disabled: isLoading || !draft.title?.trim(), children: "Save as draft & finish later" }), _jsx(Button, { variant: "primary", onClick: () => void handlePublish(), isLoading: isLoading, disabled: isLoading, children: `Publish ${listingTypeLabel}` })] }) }), stepError && (_jsx(Text, { className: "text-[var(--appkit-color-error)] px-[1.25rem] pb-[0.75rem]", size: "sm", children: stepError }))] })), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(SectionForm, { sections: sections, values: draft, onChange: update, onSubmit: handlePublish, schema: sellerProductSchema, openIds: openIds, onOpenChange: setOpenIds, isLoading: isLoading, hideActions: true }) }) }));
|
|
456
492
|
}
|
|
457
493
|
// Edit mode โ FormShell with section nav + full form
|
|
458
494
|
const editSections = [
|
|
459
495
|
...EDIT_SECTIONS,
|
|
460
|
-
...(
|
|
496
|
+
...(typeSpecificSection ? [{ id: typeSpecificSection.id, label: typeSpecificSection.label }] : []),
|
|
461
497
|
];
|
|
462
|
-
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }),
|
|
498
|
+
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificSection && (_jsxs(Section, { id: typeSpecificSection.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificSection.sectionHeading ?? typeSpecificSection.label }), typeSpecificSection.render({ values: draft, onChange: update, errors: {} })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-secondary-text)]", size: "xs", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
|
|
463
499
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect,
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Alert, Badge, Div, FormField, Heading, Row, Stack, Text } from "../../../ui";
|
|
6
6
|
import { StackedViewShell } from "../../../ui";
|
|
7
7
|
import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary } from "../../../ui/forms";
|
|
8
8
|
import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddressSelectorCreate";
|
|
9
|
-
import {
|
|
9
|
+
import { SectionForm, useSectionFormNav } from "../../shell";
|
|
10
10
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize";
|
|
12
12
|
const shippingDraftSchema = z.object({
|
|
@@ -35,12 +35,6 @@ export function SellerShippingView({ apiBase = SELLER_ENDPOINTS.SHIPPING }) {
|
|
|
35
35
|
const [saving, setSaving] = useState(false);
|
|
36
36
|
const [error, setError] = useState(null);
|
|
37
37
|
const [success, setSuccess] = useState(false);
|
|
38
|
-
const [currentStep, setCurrentStep] = useState(0);
|
|
39
|
-
const { shellCtx, setFieldError, clearErrors, validate } = useFormShellState(shippingDraftSchema);
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
validate(draft);
|
|
42
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
-
}, [draft, validate]);
|
|
44
38
|
useEffect(() => {
|
|
45
39
|
fetch(apiBase)
|
|
46
40
|
.then((r) => r.json())
|
|
@@ -105,33 +99,42 @@ export function SellerShippingView({ apiBase = SELLER_ENDPOINTS.SHIPPING }) {
|
|
|
105
99
|
}
|
|
106
100
|
};
|
|
107
101
|
const busy = loading || saving;
|
|
108
|
-
const
|
|
102
|
+
const sections = [
|
|
109
103
|
{
|
|
104
|
+
id: "carrier",
|
|
110
105
|
label: "Carrier",
|
|
106
|
+
required: true,
|
|
107
|
+
quick: true,
|
|
111
108
|
fields: ["customCarrierName", "customShippingPrice"],
|
|
112
109
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Shipping Carrier" }), current && (_jsx(Row, { className: "mb-2", align: "center", gap: "sm", children: _jsx(Badge, { variant: current.isConfigured ? "success" : "warning", children: current.isConfigured ? "Configured" : "Not configured" }) })), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Set a fixed shipping fee and carrier name for all orders. You enter the carrier and tracking number yourself when you ship each order." }), _jsxs(Stack, { gap: "md", className: "mt-2", children: [_jsx(FormField, { name: "customCarrierName", label: "Carrier Name", type: "text", value: values.customCarrierName, onChange: (v) => onChange({ customCarrierName: v }), placeholder: "e.g. India Post, DTDC, Delhivery", disabled: busy }), _jsx(FormField, { name: "customShippingPrice", label: "Shipping Price (\u20B9)", type: "number", value: values.customShippingPrice, onChange: (v) => onChange({ customShippingPrice: v }), placeholder: "0 for free shipping", helpText: "Charged to buyer at checkout. Enter 0 for free shipping.", disabled: busy })] })] })),
|
|
113
110
|
},
|
|
114
111
|
{
|
|
112
|
+
id: "pickup-address",
|
|
115
113
|
label: "Pickup Address",
|
|
116
114
|
fields: ["pickupAddressId"],
|
|
117
115
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Pickup Address" }), current?.pickupAddress && (_jsxs(Alert, { variant: "info", children: ["Current pickup: ", current.pickupAddress.locationName ?? "", current.pickupAddress.city ? `, ${current.pickupAddress.city}` : ""] })), _jsx(StoreAddressSelectorCreate, { value: values.pickupAddressId, onChange: (id) => onChange({ pickupAddressId: id }), label: "Pickup Address", disabled: busy })] })),
|
|
118
116
|
},
|
|
119
117
|
{
|
|
118
|
+
id: "rules",
|
|
120
119
|
label: "Rules",
|
|
121
120
|
fields: ["freeShippingThreshold", "fragileSurcharge"],
|
|
122
121
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Shipping Rules" }), _jsx(FormField, { name: "freeShippingThreshold", label: "Free Shipping Threshold (\u20B9)", type: "number", value: values.freeShippingThreshold, onChange: (v) => onChange({ freeShippingThreshold: v }), placeholder: "e.g. 500 \u2014 orders above this get free shipping", helpText: "Leave blank or 0 to disable free shipping offers.", disabled: busy }), _jsx(FormField, { name: "fragileSurcharge", label: "Fragile Item Surcharge (\u20B9)", type: "number", value: values.fragileSurcharge, onChange: (v) => onChange({ fragileSurcharge: v }), placeholder: "e.g. 50 \u2014 added for items marked fragile", helpText: "Leave blank or 0 to disable the fragile surcharge.", disabled: busy })] })),
|
|
123
122
|
},
|
|
124
123
|
];
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
/*
|
|
125
|
+
* `useSectionFormNav` replaces the hand-rolled fieldToStepIndex map and the
|
|
126
|
+
* manually-composed wizardShellCtx. The old shape had to be kept in sync by
|
|
127
|
+
* hand โ and `useMemo(..., [steps.length])` meant a step whose FIELDS
|
|
128
|
+
* changed without changing the COUNT never recomputed its map, so
|
|
129
|
+
* FormErrorSummary could jump to the wrong step.
|
|
130
|
+
*/
|
|
131
|
+
const { openIds, setOpenIds, goToSection, fieldToSectionIndex, sectionMeta } = useSectionFormNav(sections, draft);
|
|
132
|
+
const { shellCtx, setFieldError, clearErrors, validate } = useFormShellState(shippingDraftSchema, { sections: sectionMeta, onGoToSection: goToSection, fieldToSectionIndex });
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
validate(draft);
|
|
131
135
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
-
}, [
|
|
133
|
-
const wizardShellCtx = useMemo(() => ({ ...shellCtx, fieldToStepIndex, goToStep: (n) => setCurrentStep(n) }), [shellCtx, fieldToStepIndex, setCurrentStep]);
|
|
136
|
+
}, [draft, validate]);
|
|
134
137
|
return (_jsx(StackedViewShell, { portal: "seller", title: "Shipping Configuration", sections: [
|
|
135
|
-
_jsxs(Div, { children: [error && _jsx(Alert, { variant: "error", className: "mb-4", children: error }), success && _jsx(Alert, { variant: "success", className: "mb-4", children: "Shipping configuration saved." }), _jsxs(FormShellContext.Provider, { value:
|
|
138
|
+
_jsxs(Div, { children: [error && _jsx(Alert, { variant: "error", className: "mb-4", children: error }), success && _jsx(Alert, { variant: "success", className: "mb-4", children: "Shipping configuration saved." }), _jsxs(FormShellContext.Provider, { value: shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: update, onSubmit: handleSave, schema: shippingDraftSchema, openIds: openIds, onOpenChange: setOpenIds, submitLabel: "Save Configuration", isLoading: busy })] })] }, "shipping"),
|
|
136
139
|
] }));
|
|
137
140
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect,
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { StackedViewShell } from "../../../ui";
|
|
6
6
|
import { Alert, Div, FormField, FormGroup, Heading, Stack, Text, Toggle, } from "../../../ui";
|
|
7
7
|
import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary } from "../../../ui/forms";
|
|
8
8
|
import { ImageUpload, useMediaUpload } from "../../media";
|
|
9
|
-
import {
|
|
9
|
+
import { SectionForm, useSectionFormNav, useFormShell } from "../../shell";
|
|
10
10
|
// Small local schema for the storefront form-input shape (not the broader
|
|
11
11
|
// `sellerStoreSchema`, which requires id/storeSlug/ownerId/status/createdAt โ
|
|
12
12
|
// fields this editable draft never has, so applying it directly would show
|
|
@@ -23,14 +23,8 @@ export function SellerStorefrontView({ initialValues, onSave, isLoading = false,
|
|
|
23
23
|
const [draft, setDraft] = useState(initialValues ?? {});
|
|
24
24
|
const [saving, setSaving] = useState(false);
|
|
25
25
|
const [saved, setSaved] = useState(false);
|
|
26
|
-
const [currentStep, setCurrentStep] = useState(0);
|
|
27
26
|
const { markDirty, markClean } = useFormShell();
|
|
28
27
|
const { upload } = useMediaUpload();
|
|
29
|
-
const { shellCtx, setFieldError, clearErrors, validate } = useFormShellState(storefrontDraftSchema);
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
validate(draft);
|
|
32
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
-
}, [draft, validate]);
|
|
34
28
|
const update = useCallback((partial) => {
|
|
35
29
|
setDraft((prev) => ({ ...prev, ...partial }));
|
|
36
30
|
markDirty();
|
|
@@ -54,39 +48,47 @@ export function SellerStorefrontView({ initialValues, onSave, isLoading = false,
|
|
|
54
48
|
}
|
|
55
49
|
};
|
|
56
50
|
const busy = saving || isLoading;
|
|
57
|
-
const
|
|
51
|
+
const sections = [
|
|
58
52
|
{
|
|
53
|
+
id: "store-identity",
|
|
59
54
|
label: "Store Identity",
|
|
55
|
+
required: true,
|
|
56
|
+
quick: true,
|
|
60
57
|
fields: ["storeName", "storeDescription", "storeCategory", "bio"],
|
|
61
|
-
validate: (values) => !values.storeName?.trim() ? "Store name is required" : null,
|
|
62
58
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Store Identity" }), _jsx(FormField, { name: "storeName", label: "Store Name", type: "text", value: values.storeName ?? "", onChange: (v) => onChange({ storeName: v }), placeholder: "e.g. Pok\u00E9mon Palace", disabled: busy }), _jsx(FormField, { name: "storeCategory", label: "Store Category", type: "text", value: values.storeCategory ?? "", onChange: (v) => onChange({ storeCategory: v }), placeholder: "e.g. Trading Cards, Action Figures", disabled: busy }), _jsx(FormField, { name: "bio", label: "Short Bio (shown in search results)", type: "textarea", value: values.bio ?? "", onChange: (v) => onChange({ bio: v }), placeholder: "One-liner about your store (max 300 chars)", disabled: busy }), _jsx(FormField, { name: "storeDescription", label: "Full Description", type: "textarea", value: values.storeDescription ?? "", onChange: (v) => onChange({ storeDescription: v }), placeholder: "Detailed store description shown on your public store page\u2026", disabled: busy })] })),
|
|
63
59
|
},
|
|
64
60
|
{
|
|
61
|
+
id: "branding",
|
|
65
62
|
label: "Branding",
|
|
66
63
|
fields: ["storeLogoURL", "storeBannerURL"],
|
|
67
64
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Branding" }), _jsxs(FormGroup, { columns: 2, children: [_jsx(ImageUpload, { label: "Store Logo", currentImage: values.storeLogoURL, onUpload: (file) => upload(file, "stores", true, { type: "store-logo", store: storeSlug }), onChange: (url) => onChange({ storeLogoURL: url }), helperText: "Square image, min 200\u00D7200px" }), _jsx(ImageUpload, { label: "Store Banner", currentImage: values.storeBannerURL, onUpload: (file) => upload(file, "stores", true, { type: "store-banner", store: storeSlug }), onChange: (url) => onChange({ storeBannerURL: url }), helperText: "Recommended: 1200\u00D7300px" })] })] })),
|
|
68
65
|
},
|
|
69
66
|
{
|
|
67
|
+
id: "policies",
|
|
70
68
|
label: "Policies",
|
|
71
69
|
fields: ["returnPolicy", "shippingPolicy"],
|
|
72
70
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Policies" }), _jsx(FormField, { name: "returnPolicy", label: "Return Policy", type: "textarea", value: values.returnPolicy ?? "", onChange: (v) => onChange({ returnPolicy: v }), placeholder: "Describe your return policy\u2026", disabled: busy }), _jsx(FormField, { name: "shippingPolicy", label: "Shipping Policy", type: "textarea", value: values.shippingPolicy ?? "", onChange: (v) => onChange({ shippingPolicy: v }), placeholder: "How and when you ship orders\u2026", disabled: busy })] })),
|
|
73
71
|
},
|
|
74
72
|
{
|
|
73
|
+
id: "contact-visibility",
|
|
75
74
|
label: "Contact & Visibility",
|
|
76
75
|
fields: ["website", "location", "socialLinks", "isPublic", "isVacationMode", "vacationMessage"],
|
|
77
76
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Contact & Social" }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "website", label: "Website", type: "text", value: values.website ?? "", onChange: (v) => onChange({ website: v }), placeholder: "https://example.com", disabled: busy }), _jsx(FormField, { name: "location", label: "Location", type: "text", value: values.location ?? "", onChange: (v) => onChange({ location: v }), placeholder: "e.g. Mumbai, India", disabled: busy })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "twitter", label: "Twitter / X URL", type: "text", value: values.socialLinks?.twitter ?? "", onChange: (v) => onChange({ socialLinks: { ...values.socialLinks, twitter: v } }), placeholder: "https://twitter.com/...", disabled: busy }), _jsx(FormField, { name: "instagram", label: "Instagram URL", type: "text", value: values.socialLinks?.instagram ?? "", onChange: (v) => onChange({ socialLinks: { ...values.socialLinks, instagram: v } }), placeholder: "https://instagram.com/...", disabled: busy })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "facebook", label: "Facebook URL", type: "text", value: values.socialLinks?.facebook ?? "", onChange: (v) => onChange({ socialLinks: { ...values.socialLinks, facebook: v } }), placeholder: "https://facebook.com/...", disabled: busy }), _jsx(FormField, { name: "linkedin", label: "LinkedIn URL", type: "text", value: values.socialLinks?.linkedin ?? "", onChange: (v) => onChange({ socialLinks: { ...values.socialLinks, linkedin: v } }), placeholder: "https://linkedin.com/...", disabled: busy })] }), _jsx(Heading, { level: 4, className: "mt-4 mb-2", children: "Visibility" }), _jsx(Toggle, { checked: values.isPublic !== false, onChange: (checked) => onChange({ isPublic: checked }), label: "Make store public \u2014 visible to all buyers", disabled: busy }), values.isPublic === false && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Your store is hidden. Existing orders are unaffected." })), _jsx(Heading, { level: 4, className: "mt-4 mb-2", children: "Vacation Mode" }), _jsx(Toggle, { checked: !!values.isVacationMode, onChange: (checked) => onChange({ isVacationMode: checked }), label: "Enable vacation mode \u2014 your store will show a notice to buyers", disabled: busy }), values.isVacationMode && (_jsx(FormField, { name: "vacationMessage", label: "Vacation Message", type: "textarea", value: values.vacationMessage ?? "", onChange: (v) => onChange({ vacationMessage: v }), placeholder: "I'm away until\u2026 Orders placed now will ship when I return.", disabled: busy }))] })),
|
|
78
77
|
},
|
|
79
78
|
];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
/*
|
|
80
|
+
* `useSectionFormNav` replaces the hand-rolled fieldToStepIndex map and the
|
|
81
|
+
* manually-composed wizardShellCtx. The old `useMemo(..., [steps.length])`
|
|
82
|
+
* meant a step whose FIELDS changed without changing the COUNT never
|
|
83
|
+
* recomputed its map, so the error summary could jump to the wrong step.
|
|
84
|
+
*/
|
|
85
|
+
const { openIds, setOpenIds, goToSection, fieldToSectionIndex, sectionMeta } = useSectionFormNav(sections, draft);
|
|
86
|
+
const { shellCtx, setFieldError, clearErrors, validate } = useFormShellState(storefrontDraftSchema, { sections: sectionMeta, onGoToSection: goToSection, fieldToSectionIndex });
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
validate(draft);
|
|
86
89
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
-
}, [
|
|
88
|
-
const wizardShellCtx = useMemo(() => ({ ...shellCtx, fieldToStepIndex, goToStep: (n) => setCurrentStep(n) }), [shellCtx, fieldToStepIndex, setCurrentStep]);
|
|
90
|
+
}, [draft, validate]);
|
|
89
91
|
return (_jsx(StackedViewShell, { portal: "seller", title: "Storefront Settings", sections: [
|
|
90
|
-
_jsxs(Div, { children: [saved && (_jsx(Alert, { variant: "success", className: "mb-6", children: "Changes saved successfully." })), _jsxs(FormShellContext.Provider, { value:
|
|
92
|
+
_jsxs(Div, { children: [saved && (_jsx(Alert, { variant: "success", className: "mb-6", children: "Changes saved successfully." })), _jsxs(FormShellContext.Provider, { value: shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: update, onSubmit: handleSave, schema: storefrontDraftSchema, openIds: openIds, onOpenChange: setOpenIds, submitLabel: "Save Storefront", isLoading: busy })] })] }, "stepform"),
|
|
91
93
|
] }));
|
|
92
94
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
/**
|
|
3
3
|
* Carries the `schema` prop passed to `<FormShell>` down to any nested
|
|
4
|
-
* `<
|
|
4
|
+
* `<SectionForm>` that doesn't declare its own `schema`/`SectionDef.schema` โ
|
|
5
5
|
* `FormShell` (chrome) has no form values of its own to validate, so it
|
|
6
6
|
* can't run `.safeParse()` itself; this is what makes the prop load-bearing
|
|
7
7
|
* instead of silently discarded.
|
|
@@ -18,7 +18,7 @@ export interface FormShellProps {
|
|
|
18
18
|
breadcrumb?: string;
|
|
19
19
|
isDirty?: boolean;
|
|
20
20
|
isLoading?: boolean;
|
|
21
|
-
/** Left-nav anchor sections
|
|
21
|
+
/** Left-nav anchor sections. */
|
|
22
22
|
sections?: FormShellSection[];
|
|
23
23
|
onSaveDraft?: () => void | Promise<void>;
|
|
24
24
|
onPublish?: () => void | Promise<void>;
|
|
@@ -52,8 +52,8 @@ export interface FormShellProps {
|
|
|
52
52
|
/**
|
|
53
53
|
* Zod schema covering the form's fields. `FormShell` has no form values of
|
|
54
54
|
* its own, so it can't `.safeParse()` this directly โ instead it provides
|
|
55
|
-
* the schema via `FormSchemaContext` to any nested `<
|
|
56
|
-
* doesn't declare its own `schema`/`
|
|
55
|
+
* the schema via `FormSchemaContext` to any nested `<SectionForm>` that
|
|
56
|
+
* doesn't declare its own `schema`/`SectionDef.schema`, so existing
|
|
57
57
|
* `<FormShell schema={x}>` call sites start validating with zero changes.
|
|
58
58
|
* `audit-form-schema` requires every callsite to declare one.
|
|
59
59
|
*/
|
|
@@ -9,7 +9,7 @@ import { FORM_ACTION_META, FORM_ACTION_ID } from "../products/constants/action-d
|
|
|
9
9
|
import { Div, Row, Span, Stack, Text } from "../../ui";
|
|
10
10
|
/**
|
|
11
11
|
* Carries the `schema` prop passed to `<FormShell>` down to any nested
|
|
12
|
-
* `<
|
|
12
|
+
* `<SectionForm>` that doesn't declare its own `schema`/`SectionDef.schema` โ
|
|
13
13
|
* `FormShell` (chrome) has no form values of its own to validate, so it
|
|
14
14
|
* can't run `.safeParse()` itself; this is what makes the prop load-bearing
|
|
15
15
|
* instead of silently discarded.
|
|
@@ -4,8 +4,6 @@ export type { CommandPaletteGroup, CommandPaletteProps } from "./CommandPalette"
|
|
|
4
4
|
export { CommandPalette, useCommandPaletteHotkey } from "./CommandPalette";
|
|
5
5
|
export type { QuickFieldType, QuickFieldDef, QuickFormDrawerProps } from "./QuickFormDrawer";
|
|
6
6
|
export { QuickFormDrawer } from "./QuickFormDrawer";
|
|
7
|
-
export type { StepDef, StepFormProps, StepFormActionsProps } from "./StepForm";
|
|
8
|
-
export { StepForm, StepFormActions, StepIndicator } from "./StepForm";
|
|
9
7
|
export type { SectionDef, SectionFormProps, UseSectionFormNavResult, } from "./SectionForm";
|
|
10
8
|
export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./SectionForm";
|
|
11
9
|
export type { EntityFormDefinition } from "./entity-form";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { FormShell, useFormShell, FormSchemaContext } from "./FormShell";
|
|
2
2
|
export { CommandPalette, useCommandPaletteHotkey } from "./CommandPalette";
|
|
3
3
|
export { QuickFormDrawer } from "./QuickFormDrawer";
|
|
4
|
-
export { StepForm, StepFormActions, StepIndicator } from "./StepForm";
|
|
5
4
|
export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./SectionForm";
|
|
6
5
|
export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDerive, assertNoDeriveCollision, humaniseFieldName, } from "./entity-form";
|
|
7
6
|
export { ui, annotate, ROW_COLUMNS, inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, schemaEnumOptions, schemaIsOptional, objectShape, } from "./field-ui-meta";
|
|
@@ -3083,6 +3083,30 @@ const rawTesterChecklistItems = [
|
|
|
3083
3083
|
description: "Both PATCH routes previously spread the raw request body into Firestore with no schema, so any key and any status string could be written. Confirm the normal Take / Action / Dismiss and Approve / Reject flows still all work โ that is what the tightening must not have broken.",
|
|
3084
3084
|
href: "/admin/reports",
|
|
3085
3085
|
},
|
|
3086
|
+
{
|
|
3087
|
+
key: "listing-form-every-section-reachable",
|
|
3088
|
+
label: "Creating a listing shows ALL sections at once โ you can open Pricing before finishing Basic, and nothing blocks you",
|
|
3089
|
+
description: "The last step wizard was removed 2026-08-26. It gated Next on the current step's validate, so a seller who could not satisfy step 2 could not reach step 5 to find out what else was needed. Open /store/products/new, expand a later section immediately, and confirm it opens. The required section (Basic) stays open and cannot be collapsed.",
|
|
3090
|
+
href: "/store/products/new",
|
|
3091
|
+
},
|
|
3092
|
+
{
|
|
3093
|
+
key: "listing-form-errors-land-on-fields",
|
|
3094
|
+
label: "Publishing an incomplete listing puts each error on ITS OWN FIELD, and the summary links jump to that section",
|
|
3095
|
+
description: "The per-step validate callbacks returned a single string rendered as one banner over the whole step. They are per-section Zod schemas now, so a missing auction start bid reports on the start-bid field. Press Publish with several sections incomplete and confirm each message appears beside its input, and that clicking an entry in the summary opens the right section.",
|
|
3096
|
+
href: "/store/products/new",
|
|
3097
|
+
},
|
|
3098
|
+
{
|
|
3099
|
+
key: "listing-form-save-draft-anywhere",
|
|
3100
|
+
label: "\"Save as draft & finish later\" is available from any section, not only the first",
|
|
3101
|
+
description: "It used to be gated on `currentStep === 0`, which was a wizard artefact โ a draft is a draft whichever section you are looking at. Fill in a title, scroll to a later section, and confirm the button is still there and still saves.",
|
|
3102
|
+
href: "/store/products/new",
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
key: "seller-shipping-storefront-sections",
|
|
3106
|
+
label: "Shipping Configuration and Storefront Settings are collapsible sections with one Save at the bottom โ no Next/Back",
|
|
3107
|
+
description: "Both were 3- and 4-step wizards until 2026-08-26. Check that every section is reachable immediately, that the error summary jumps to the right one, and that a single Save covers whichever sections you edited.",
|
|
3108
|
+
href: "/store/shipping",
|
|
3109
|
+
},
|
|
3086
3110
|
{
|
|
3087
3111
|
key: "admin-tables-render-badges-not-text",
|
|
3088
3112
|
label: "Admin list tables show a coloured STATUS BADGE, a thumbnail and formatted money โ not raw text",
|
package/dist/index.d.ts
CHANGED
|
@@ -3203,8 +3203,6 @@ export { CommandPalette, useCommandPaletteHotkey } from "./features/shell/index"
|
|
|
3203
3203
|
export type { CommandPaletteGroup, CommandPaletteProps } from "./features/shell/index";
|
|
3204
3204
|
export { QuickFormDrawer } from "./features/shell/index";
|
|
3205
3205
|
export type { QuickFieldType, QuickFieldDef, QuickFormDrawerProps } from "./features/shell/index";
|
|
3206
|
-
export { StepForm, StepFormActions, StepIndicator } from "./features/shell/index";
|
|
3207
|
-
export type { StepDef, StepFormProps, StepFormActionsProps } from "./features/shell/index";
|
|
3208
3206
|
export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./features/shell/index";
|
|
3209
3207
|
export type { SectionDef, SectionFormProps, UseSectionFormNavResult } from "./features/shell/index";
|
|
3210
3208
|
export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDerive, assertNoDeriveCollision, humaniseFieldName, } from "./features/shell/index";
|
|
@@ -3294,3 +3292,5 @@ export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, feat
|
|
|
3294
3292
|
export { changeEmailSchema } from "./features/account/schemas/index";
|
|
3295
3293
|
export { adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema, } from "./features/admin/schemas/admin-user-form";
|
|
3296
3294
|
export { siteSettingsFormSchema } from "./features/admin/schemas/site-settings-form";
|
|
3295
|
+
export { createReviewSchema as reviewSubmitSchema } from "./_internal/shared/features/reviews/schema";
|
|
3296
|
+
export { wishlistAddSchema } from "./schemas/api-routes";
|
package/dist/index.js
CHANGED
|
@@ -5608,9 +5608,8 @@ export { FormShell, useFormShell } from "./features/shell/index";
|
|
|
5608
5608
|
export { CommandPalette, useCommandPaletteHotkey } from "./features/shell/index";
|
|
5609
5609
|
// [CLIENT] QuickFormDrawer รขโฌ" 40% desktop / 100% mobile auto-rendered field drawer.
|
|
5610
5610
|
export { QuickFormDrawer } from "./features/shell/index";
|
|
5611
|
-
// [CLIENT]
|
|
5612
|
-
|
|
5613
|
-
// [CLIENT] SectionForm โ collapsible-section form engine replacing StepForm.
|
|
5611
|
+
// [CLIENT] SectionForm โ collapsible-section form engine. Replaced StepForm,
|
|
5612
|
+
// which was deleted 2026-08-26.
|
|
5614
5613
|
// Required section first and always open, one shared submit at the bottom, no
|
|
5615
5614
|
// per-section gate blocking a later section.
|
|
5616
5615
|
export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./features/shell/index";
|
|
@@ -5703,3 +5702,12 @@ export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, feat
|
|
|
5703
5702
|
export { changeEmailSchema } from "./features/account/schemas/index";
|
|
5704
5703
|
export { adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema, } from "./features/admin/schemas/admin-user-form";
|
|
5705
5704
|
export { siteSettingsFormSchema } from "./features/admin/schemas/site-settings-form";
|
|
5705
|
+
/*
|
|
5706
|
+
* Two schemas that existed and whose own routes did not use them.
|
|
5707
|
+
* `postApiWishlist` had sat in SCHEMAS.api since the registry was seeded while
|
|
5708
|
+
* `/api/wishlist` read its body raw; `createReviewSchema` was reachable only
|
|
5709
|
+
* through a server action while `POST /api/reviews` passed `body as any`.
|
|
5710
|
+
* Re-exported here under names that read clearly at a route call site.
|
|
5711
|
+
*/
|
|
5712
|
+
export { createReviewSchema as reviewSubmitSchema } from "./_internal/shared/features/reviews/schema";
|
|
5713
|
+
export { wishlistAddSchema } from "./schemas/api-routes";
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import type { ApiRouteSchema } from "./types";
|
|
3
|
+
/** The body shape, named so a route can import it directly. */
|
|
4
|
+
export declare const wishlistAddSchema: z.ZodObject<{
|
|
5
|
+
productId: z.ZodString;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
productId: string;
|
|
8
|
+
}, {
|
|
9
|
+
productId: string;
|
|
10
|
+
}>;
|
|
2
11
|
export declare const postApiWishlist: ApiRouteSchema;
|
|
3
12
|
export declare const postApiAuthSession: ApiRouteSchema;
|
|
4
13
|
export declare const postApiAdminProductsIdGroup: ApiRouteSchema;
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
// as the highest unknown-leakage offenders). W5โW7 will register the rest.
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
// โโ POST /api/wishlist โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
19
|
+
/** The body shape, named so a route can import it directly. */
|
|
20
|
+
export const wishlistAddSchema = z.object({
|
|
21
|
+
productId: z.string().min(1),
|
|
22
|
+
});
|
|
19
23
|
export const postApiWishlist = {
|
|
20
|
-
body:
|
|
21
|
-
productId: z.string().min(1),
|
|
22
|
-
}),
|
|
24
|
+
body: wishlistAddSchema,
|
|
23
25
|
};
|
|
24
26
|
// โโ POST /api/auth/session โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
25
27
|
export const postApiAuthSession = {
|