enh
This commit is contained in:
parent
9d2e01c6d2
commit
21002332fb
1 changed files with 61 additions and 22 deletions
|
|
@ -123,21 +123,37 @@ const productValidationSchema = Yup.object().shape({
|
||||||
}
|
}
|
||||||
return true
|
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
|
if (!Array.isArray(variants)) return true
|
||||||
for (const variant of variants) {
|
for (const variant of variants) {
|
||||||
const attrs = variant?.attributes || []
|
const attrs = variant?.attributes || []
|
||||||
const hasQuantity = attrs.some(
|
const quantityCount = attrs.filter(
|
||||||
(a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity'
|
(a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity'
|
||||||
)
|
).length
|
||||||
if (!hasQuantity) {
|
if (quantityCount === 0) {
|
||||||
return this.createError({ message: 'Every SKU must have a quantity feature' })
|
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
|
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>(({
|
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
mode,
|
mode,
|
||||||
initialValues,
|
initialValues,
|
||||||
|
|
@ -188,6 +204,20 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
initialValues={formInitialValues}
|
initialValues={formInitialValues}
|
||||||
validationSchema={productValidationSchema}
|
validationSchema={productValidationSchema}
|
||||||
onSubmit={(values) => {
|
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) =>
|
const images = variantImages.map((imgs) =>
|
||||||
imgs.map((img) => ({ url: img.imgUrl, mimeType: img.mimeType }))
|
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
|
enableReinitialize
|
||||||
>
|
>
|
||||||
|
|
@ -323,13 +353,24 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
|
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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 key={aIndex} style={tw`flex-row items-center mb-2 gap-2`}>
|
||||||
<View style={tw`flex-1`}>
|
<View style={tw`flex-1`}>
|
||||||
<MyTextInput
|
<MyTextInput
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
value={attr.featureName ?? ''}
|
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)}
|
editable={!isQuantityFeature(attr)}
|
||||||
style={isQuantityFeature(attr) ? tw`text-gray-400` : undefined}
|
style={isQuantityFeature(attr) ? tw`text-gray-400` : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
@ -341,13 +382,14 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
|
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{variant.attributes.length > 1 && !isQuantityFeature(attr) && (
|
{canDelete && (
|
||||||
<TouchableOpacity onPress={() => removeAttr(aIndex)}>
|
<TouchableOpacity onPress={() => removeAttr(aIndex)}>
|
||||||
<MaterialIcons name="close" size={18} color="#EF4444" />
|
<MaterialIcons name="close" size={18} color="#EF4444" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
<View style={tw`flex-row items-center self-start`}>
|
<View style={tw`flex-row items-center self-start`}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => pushAttr(defaultAttribute())}
|
onPress={() => pushAttr(defaultAttribute())}
|
||||||
|
|
@ -534,18 +576,10 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
const validationErrors = await validateForm()
|
const validationErrors = await validateForm()
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
const variantsError = validationErrors.variants
|
const variantsMessages = collectErrorMessages(validationErrors.variants)
|
||||||
const firstVariantErrors = Array.isArray(variantsError)
|
const allMessages = collectErrorMessages(validationErrors)
|
||||||
? (variantsError[0] as Record<string, unknown> | undefined) || {}
|
const message = variantsMessages[0] || allMessages[0] || 'Please fix the highlighted fields'
|
||||||
: {}
|
Alert.alert('Check your form', String(message))
|
||||||
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'))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleSubmit()
|
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'}`}
|
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`}>
|
<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>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue