diff --git a/apps/admin-ui/src/components/ProductForm.tsx b/apps/admin-ui/src/components/ProductForm.tsx index 091b84d..7d78f0d 100644 --- a/apps/admin-ui/src/components/ProductForm.tsx +++ b/apps/admin-ui/src/components/ProductForm.tsx @@ -123,21 +123,37 @@ const productValidationSchema = Yup.object().shape({ } return true }) - .test('quantity-feature', 'Every SKU must have a quantity feature', function (variants) { + .test('quantity-feature', 'Each SKU must have exactly one quantity feature', function (variants) { if (!Array.isArray(variants)) return true for (const variant of variants) { const attrs = variant?.attributes || [] - const hasQuantity = attrs.some( + const quantityCount = attrs.filter( (a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity' - ) - if (!hasQuantity) { + ).length + if (quantityCount === 0) { return this.createError({ message: 'Every SKU must have a quantity feature' }) } + if (quantityCount > 1) { + return this.createError({ message: 'Each SKU can have only one quantity feature' }) + } } return true }), }) +// Collect every string message from a Formik errors tree (objects, arrays, strings). +// Variant-level errors (e.g. 'Each SKU can have only one quantity feature') can get +// hidden by Formik when index-level errors are also present, so we walk everything. +const collectErrorMessages = (errors: unknown, depth = 0): string[] => { + if (depth > 6) return [] + if (typeof errors === 'string') return [errors] + if (Array.isArray(errors)) return errors.flatMap((e) => collectErrorMessages(e, depth + 1)) + if (errors && typeof errors === 'object') { + return Object.values(errors).flatMap((e) => collectErrorMessages(e, depth + 1)) + } + return [] +} + const ProductForm = forwardRef(({ mode, initialValues, @@ -188,6 +204,20 @@ const ProductForm = forwardRef(({ initialValues={formInitialValues} validationSchema={productValidationSchema} onSubmit={(values) => { + // Normalize feature names before sending to backend: the mandatory + // 'quantity' feature must always go out lowercase (case-insensitive + // matching means 'Quantity'/'QUANTITY' are treated the same), and + // names are trimmed like the backend does. + const normalizedValues: ProductFormData = { + ...values, + variants: values.variants.map((v) => ({ + ...v, + attributes: v.attributes.map((a) => ({ + ...a, + featureName: isQuantityFeature(a) ? 'quantity' : (a.featureName ?? '').trim() || null, + })), + })), + } const images = variantImages.map((imgs) => imgs.map((img) => ({ url: img.imgUrl, mimeType: img.mimeType })) ) @@ -203,7 +233,7 @@ const ProductForm = forwardRef(({ }) }) } - onSubmit(values, images, deletedKeys) + onSubmit(normalizedValues, images, deletedKeys) }} enableReinitialize > @@ -323,13 +353,24 @@ const ProductForm = forwardRef(({ Add - {variant.attributes.map((attr, aIndex) => ( + {variant.attributes.map((attr, aIndex) => { + const quantityCount = variant.attributes.filter(isQuantityFeature).length + // Quantity features are locked (non-editable), but if there + // are duplicates (e.g. 'quantity' + 'Quantity') the user must + // be able to delete the extras to fix the form. + const canDelete = variant.attributes.length > 1 && (!isQuantityFeature(attr) || quantityCount > 1) + return ( + setFieldValue( + `variants.${vIndex}.attributes.${aIndex}.featureName`, + text.trim().toLowerCase() === 'quantity' ? 'quantity' : text + ) + } editable={!isQuantityFeature(attr)} style={isQuantityFeature(attr) ? tw`text-gray-400` : undefined} /> @@ -341,13 +382,14 @@ const ProductForm = forwardRef(({ onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)} /> - {variant.attributes.length > 1 && !isQuantityFeature(attr) && ( + {canDelete && ( removeAttr(aIndex)}> )} - ))} + ) + })} pushAttr(defaultAttribute())} @@ -534,18 +576,10 @@ const ProductForm = forwardRef(({ onPress={async () => { const validationErrors = await validateForm() if (Object.keys(validationErrors).length > 0) { - const variantsError = validationErrors.variants - const firstVariantErrors = Array.isArray(variantsError) - ? (variantsError[0] as Record | undefined) || {} - : {} - const message = (firstVariantErrors.price as string | undefined) - || (firstVariantErrors.marketPrice as string | undefined) - || (firstVariantErrors.flashPrice as string | undefined) - || (firstVariantErrors.attributes as string | undefined) - || validationErrors.name - || validationErrors.storeId - || variantsError - Alert.alert('Check your form', String(message || 'Please fix the highlighted fields')) + const variantsMessages = collectErrorMessages(validationErrors.variants) + const allMessages = collectErrorMessages(validationErrors) + const message = variantsMessages[0] || allMessages[0] || 'Please fix the highlighted fields' + Alert.alert('Check your form', String(message)) return } handleSubmit() @@ -554,7 +588,12 @@ const ProductForm = forwardRef(({ style={tw`px-4 py-3 rounded-lg shadow-lg items-center mt-4 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`} > - {isLoading ? 'Creating...' : 'Create Product'} + {(() => { + if (mode === 'edit') { + return isLoading ? 'Saving...' : 'Save Changes' + } + return isLoading ? 'Creating...' : 'Create Product' + })()}