This commit is contained in:
shafi54 2026-08-15 00:35:47 +05:30
parent 9d2e01c6d2
commit 21002332fb

View file

@ -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<ProductFormRef, ProductFormProps>(({
mode,
initialValues,
@ -188,6 +204,20 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
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<ProductFormRef, ProductFormProps>(({
})
})
}
onSubmit(values, images, deletedKeys)
onSubmit(normalizedValues, images, deletedKeys)
}}
enableReinitialize
>
@ -323,13 +353,24 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
</TouchableOpacity>
</View>
{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 (
<View key={aIndex} style={tw`flex-row items-center mb-2 gap-2`}>
<View style={tw`flex-1`}>
<MyTextInput
placeholder="Name"
value={attr.featureName ?? ''}
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureName`)}
onChangeText={(text) =>
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<ProductFormRef, ProductFormProps>(({
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
/>
</View>
{variant.attributes.length > 1 && !isQuantityFeature(attr) && (
{canDelete && (
<TouchableOpacity onPress={() => removeAttr(aIndex)}>
<MaterialIcons name="close" size={18} color="#EF4444" />
</TouchableOpacity>
)}
</View>
))}
)
})}
<View style={tw`flex-row items-center self-start`}>
<TouchableOpacity
onPress={() => pushAttr(defaultAttribute())}
@ -534,18 +576,10 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
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<string, unknown> | 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<ProductFormRef, ProductFormProps>(({
style={tw`px-4 py-3 rounded-lg shadow-lg items-center mt-4 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
>
<MyText style={tw`text-white text-lg font-bold`}>
{isLoading ? 'Creating...' : 'Create Product'}
{(() => {
if (mode === 'edit') {
return isLoading ? 'Saving...' : 'Save Changes'
}
return isLoading ? 'Creating...' : 'Create Product'
})()}
</MyText>
</TouchableOpacity>
</ScrollView>