fix: improve product form validation and navigation guards
This commit is contained in:
@@ -19,6 +19,7 @@ interface CategoryNode {
|
||||
interface CategoryTreeSelectProps {
|
||||
value?: string;
|
||||
onChange: (categoryId: string) => void;
|
||||
onBlur?: () => void;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
@@ -60,6 +61,7 @@ function buildCategoryTree(categories: any[]): CategoryNode[] {
|
||||
export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
placeholder = 'Select Category...',
|
||||
error,
|
||||
disabled = false,
|
||||
@@ -68,7 +70,7 @@ export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||||
|
||||
|
||||
// Quick Create Drawer state
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [newCatName, setNewCatName] = useState('');
|
||||
@@ -86,12 +88,17 @@ export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
|
||||
useEffect(() => {
|
||||
const handleOutsideClick = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
setIsOpen((prev) => {
|
||||
if (prev) {
|
||||
onBlur?.();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleOutsideClick);
|
||||
return () => document.removeEventListener('mousedown', handleOutsideClick);
|
||||
}, []);
|
||||
}, [onBlur]);
|
||||
|
||||
const categoryTree = useMemo(() => buildCategoryTree(categories), [categories]);
|
||||
|
||||
@@ -138,7 +145,7 @@ export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
|
||||
|
||||
const createdId = created?.id || created?.data?.id;
|
||||
notify.success(`Category "${newCatName}" created successfully!`);
|
||||
|
||||
|
||||
await fetchCategories();
|
||||
if (createdId) {
|
||||
onChange(createdId);
|
||||
@@ -178,11 +185,10 @@ export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
|
||||
<div key={node.id} className="select-none">
|
||||
<div
|
||||
onClick={() => handleSelect(node.id)}
|
||||
className={`flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium cursor-pointer transition-colors ${
|
||||
isSelected
|
||||
? 'bg-primary/10 text-primary font-bold'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
className={`flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium cursor-pointer transition-colors ${isSelected
|
||||
? 'bg-primary/10 text-primary font-bold'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
style={{ paddingLeft: `${node.depth * 16 + 12}px` }}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@@ -233,11 +239,10 @@ export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
|
||||
{/* Trigger Button */}
|
||||
<div
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
className={`w-full border rounded-lg px-3 py-2.5 text-sm flex items-center justify-between bg-white cursor-pointer transition-all ${
|
||||
disabled ? 'bg-gray-50 opacity-60 cursor-not-allowed border-gray-200' :
|
||||
className={`w-full border rounded-lg px-3 py-2.5 text-sm flex items-center justify-between bg-white cursor-pointer transition-all ${disabled ? 'bg-gray-50 opacity-60 cursor-not-allowed border-gray-200' :
|
||||
isOpen ? 'border-primary ring-2 ring-primary/20' :
|
||||
error ? 'border-red-300' : 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
error ? 'border-red-300' : 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<Folder className={`w-4 h-4 ${selectedCategory ? 'text-primary' : 'text-gray-400'}`} />
|
||||
|
||||
@@ -77,10 +77,11 @@ export function Select({
|
||||
dropdownRef.current?.contains(e.target as Node)
|
||||
) return;
|
||||
setIsOpen(false);
|
||||
onBlur?.({ target: { name } } as any);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isOpen]);
|
||||
}, [isOpen, name, onBlur]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
|
||||
@@ -23,6 +23,7 @@ interface DynamicAttributeRendererProps {
|
||||
attribute: Attribute;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
onBlur?: () => void;
|
||||
error?: string;
|
||||
touched?: boolean;
|
||||
readOnly?: boolean;
|
||||
@@ -32,6 +33,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
attribute,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
error,
|
||||
touched,
|
||||
readOnly,
|
||||
@@ -47,6 +49,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
<textarea
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
placeholder={`Enter ${attribute.name}`}
|
||||
rows={3}
|
||||
className={`${inputClass} resize-none`}
|
||||
@@ -64,6 +67,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
const val = e.target.value;
|
||||
onChange(val === '' ? undefined : Number(val));
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
placeholder={`Enter ${attribute.name}`}
|
||||
className={inputClass}
|
||||
disabled={readOnly}
|
||||
@@ -75,6 +79,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
type="date"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
className={inputClass}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
@@ -87,6 +92,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
const val = e.target.value;
|
||||
onChange(val === 'true' ? true : val === 'false' ? false : undefined);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<option value="">Select option</option>
|
||||
@@ -101,6 +107,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
<Select
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<option value="">Select option</option>
|
||||
@@ -119,9 +126,10 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
updated = selectedValues.filter((v) => v !== optCode);
|
||||
}
|
||||
onChange(updated.join(','));
|
||||
onBlur?.(); // trigger validation immediately
|
||||
};
|
||||
return (
|
||||
<div className="space-y-2 border border-border rounded-lg p-3 bg-background/30">
|
||||
<div className="space-y-2 border border-border rounded-lg p-3 bg-background/30" onBlur={onBlur}>
|
||||
{attribute.optionsList?.map((opt) => {
|
||||
const isChecked = selectedValues.includes(opt.code);
|
||||
return (
|
||||
@@ -149,6 +157,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
placeholder={`Enter ${attribute.name}`}
|
||||
className={inputClass}
|
||||
disabled={readOnly}
|
||||
|
||||
@@ -32,6 +32,7 @@ interface DynamicAttributesSectionProps {
|
||||
errors?: Record<string, any>;
|
||||
touched?: Record<string, any>;
|
||||
onAttributeChange: (code: string, value: any) => void;
|
||||
onAttributeBlur?: (code: string) => void;
|
||||
onAddAttributeClick?: (group: any) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
@@ -43,6 +44,7 @@ export const DynamicAttributesSection: React.FC<DynamicAttributesSectionProps> =
|
||||
errors = {},
|
||||
touched = {},
|
||||
onAttributeChange,
|
||||
onAttributeBlur,
|
||||
onAddAttributeClick,
|
||||
readOnly,
|
||||
}) => {
|
||||
@@ -91,6 +93,7 @@ export const DynamicAttributesSection: React.FC<DynamicAttributesSectionProps> =
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
onAttributeChange={onAttributeChange}
|
||||
onAttributeBlur={onAttributeBlur}
|
||||
onAddAttributeClick={onAddAttributeClick}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ProductAttributeGroupProps {
|
||||
errors?: Record<string, any>;
|
||||
touched?: Record<string, any>;
|
||||
onAttributeChange: (code: string, value: any) => void;
|
||||
onAttributeBlur?: (code: string) => void;
|
||||
onAddAttributeClick?: (group: AttributeGroup) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
@@ -43,6 +44,7 @@ export const ProductAttributeGroup: React.FC<ProductAttributeGroupProps> = ({
|
||||
errors = {},
|
||||
touched = {},
|
||||
onAttributeChange,
|
||||
onAttributeBlur,
|
||||
onAddAttributeClick,
|
||||
readOnly,
|
||||
}) => {
|
||||
@@ -106,6 +108,7 @@ export const ProductAttributeGroup: React.FC<ProductAttributeGroupProps> = ({
|
||||
attribute={attr}
|
||||
value={values[attr.code]}
|
||||
onChange={(val) => onAttributeChange(attr.code, val)}
|
||||
onBlur={() => onAttributeBlur?.(attr.code)}
|
||||
error={errors[attr.code]}
|
||||
touched={touched[attr.code]}
|
||||
readOnly={readOnly}
|
||||
|
||||
@@ -28,6 +28,13 @@ import { Select } from '../../../components/customs/Select';
|
||||
import { Loader } from '../../../components/customs/Loader';
|
||||
import { CategoryTreeSelect } from '../../../components/customs/CategoryTreeSelect';
|
||||
|
||||
export function isEmptyAttributeValue(val: any): boolean {
|
||||
if (val === undefined || val === null || val === '') return true;
|
||||
if (typeof val === 'string' && val.trim() === '') return true;
|
||||
if (Array.isArray(val) && val.length === 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function NewProduct() {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -198,6 +205,7 @@ export default function NewProduct() {
|
||||
setSelectedAttributeSetObj(null);
|
||||
setCustomAddedAttributes([]);
|
||||
formik.setFieldValue('attributes', {});
|
||||
formik.setTouched({ ...formik.touched, attributes: {} });
|
||||
return;
|
||||
}
|
||||
setLoadingProduct(true);
|
||||
@@ -206,8 +214,10 @@ export default function NewProduct() {
|
||||
setSelectedAttributeSetObj(setDetails);
|
||||
setCustomAddedAttributes([]);
|
||||
formik.setFieldValue('attributes', {});
|
||||
} catch (err) {
|
||||
notify.error('Failed to load attribute set details');
|
||||
formik.setTouched({ ...formik.touched, attributes: {} });
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load attribute set details:', err);
|
||||
notify.error(`Failed to load attribute set details: ${err?.message || err}`);
|
||||
} finally {
|
||||
setLoadingProduct(false);
|
||||
}
|
||||
@@ -398,7 +408,7 @@ export default function NewProduct() {
|
||||
|
||||
// 3. Generate Validation Schema dynamically
|
||||
const dynamicValidationSchema = useMemo(() => {
|
||||
return Yup.object().shape({
|
||||
const shape: any = {
|
||||
name: Yup.string().required('Product name is required'),
|
||||
category: Yup.string().required('Category is required'),
|
||||
brand: Yup.string().required('Brand is required'),
|
||||
@@ -406,8 +416,96 @@ export default function NewProduct() {
|
||||
status: Yup.string().oneOf(['active', 'pending', 'draft', 'disabled']),
|
||||
price: Yup.string(),
|
||||
stock: Yup.number().integer().min(0, 'Stock cannot be negative'),
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const attributeShape: any = {};
|
||||
if (Array.isArray(filteredAttributesList)) {
|
||||
filteredAttributesList.forEach((attr: any) => {
|
||||
if (!attr || !attr.code) return;
|
||||
let validator: any = Yup.string();
|
||||
|
||||
const type = (attr.type || '').toLowerCase();
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
case 'decimal':
|
||||
validator = Yup.number().typeError(`${attr.name || attr.code} must be a number`);
|
||||
|
||||
// Numeric limits
|
||||
const minVal = attr.min ?? attr.min_value ?? attr.minValue;
|
||||
const maxVal = attr.max ?? attr.max_value ?? attr.maxValue;
|
||||
|
||||
if (minVal !== undefined && minVal !== null && minVal !== '') {
|
||||
validator = validator.min(Number(minVal), `${attr.name || attr.code} cannot be less than ${minVal}`);
|
||||
}
|
||||
if (maxVal !== undefined && maxVal !== null && maxVal !== '') {
|
||||
validator = validator.max(Number(maxVal), `${attr.name || attr.code} cannot be greater than ${maxVal}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
validator = Yup.date().typeError(`${attr.name || attr.code} must be a valid date`);
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
validator = Yup.boolean().typeError(`${attr.name || attr.code} must be a boolean`);
|
||||
break;
|
||||
|
||||
case 'multiselect':
|
||||
validator = Yup.string();
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
case 'select':
|
||||
case 'enumeration':
|
||||
case 'swatch':
|
||||
default:
|
||||
validator = Yup.string();
|
||||
|
||||
// String lengths
|
||||
const minLen = attr.minLength ?? attr.min_length;
|
||||
const maxLen = attr.maxLength ?? attr.max_length;
|
||||
|
||||
if (minLen !== undefined && minLen !== null && minLen !== '' && Number(minLen) > 0) {
|
||||
validator = validator.min(Number(minLen), `${attr.name || attr.code} must be at least ${minLen} characters`);
|
||||
}
|
||||
if (maxLen !== undefined && maxLen !== null && maxLen !== '' && Number(maxLen) > 0) {
|
||||
validator = validator.max(Number(maxLen), `${attr.name || attr.code} cannot exceed ${maxLen} characters`);
|
||||
}
|
||||
|
||||
// Regex pattern
|
||||
const pattern = attr.regexPattern ?? attr.regex_pattern ?? attr.pattern;
|
||||
if (pattern) {
|
||||
try {
|
||||
const regex = new RegExp(pattern);
|
||||
validator = validator.matches(regex, `${attr.name || attr.code} is invalid`);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Required flag from dynamic metadata
|
||||
if (attr.is_required || attr.isRequired) {
|
||||
validator = validator.required(`${attr.name || attr.code} is required`)
|
||||
.test('is-empty', `${attr.name || attr.code} is required`, (val: any) => {
|
||||
if (val === undefined || val === null || val === '') return false;
|
||||
if (typeof val === 'string' && val.trim() === '') return false;
|
||||
if (Array.isArray(val) && val.length === 0) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
attributeShape[attr.code] = validator;
|
||||
});
|
||||
}
|
||||
|
||||
shape.attributes = Yup.object().shape(attributeShape);
|
||||
|
||||
return Yup.object().shape(shape);
|
||||
}, [filteredAttributesList]);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
@@ -486,6 +584,18 @@ export default function NewProduct() {
|
||||
}
|
||||
});
|
||||
|
||||
const areRequiredAttributesComplete = useMemo(() => {
|
||||
if (!Array.isArray(filteredAttributesList)) return true;
|
||||
return !filteredAttributesList.some((attr: any) => {
|
||||
const isReq = attr.is_required === true || attr.isRequired === true;
|
||||
if (isReq) {
|
||||
const val = formik.values.attributes?.[attr.code];
|
||||
return isEmptyAttributeValue(val);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, [filteredAttributesList, formik.values.attributes]);
|
||||
|
||||
const currentTabs = useMemo(() => {
|
||||
const isVariant = formik.values.type === 'variant';
|
||||
const tabsList = [
|
||||
@@ -507,7 +617,30 @@ export default function NewProduct() {
|
||||
}
|
||||
}, [activeTab, currentTabs]);
|
||||
|
||||
const isNextDisabled = !formik.values.name || !formik.values.brand || !formik.values.unit || !formik.values.category || !formik.isValid;
|
||||
const isNextDisabled = useMemo(() => {
|
||||
// 1. Core General fields check (always required to proceed)
|
||||
const isGeneralInvalid = !formik.values.name || !formik.values.brand || !formik.values.unit || !formik.values.category;
|
||||
if (isGeneralInvalid) return true;
|
||||
|
||||
// 2. If on 'general' step, we only care about general fields
|
||||
if (activeTab === 'general') {
|
||||
const generalKeys = ['name', 'brand', 'unit', 'category', 'status', 'price', 'stock'];
|
||||
return Object.keys(formik.errors).some(k => generalKeys.includes(k));
|
||||
}
|
||||
|
||||
// 3. If on 'attributes' step, we must also block if any dynamic attributes are invalid or empty
|
||||
if (activeTab === 'attributes') {
|
||||
if (formik.errors.attributes && Object.keys(formik.errors.attributes).length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (!areRequiredAttributesComplete) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback: check global Formik validation status
|
||||
return !formik.isValid;
|
||||
}, [activeTab, formik.values, formik.errors, formik.isValid, filteredAttributesList, areRequiredAttributesComplete]);
|
||||
|
||||
const handleSubmitWithValidation = async (targetStatus?: string) => {
|
||||
const statusToUse = targetStatus || formik.values.status || 'draft';
|
||||
@@ -515,22 +648,7 @@ export default function NewProduct() {
|
||||
|
||||
const errors: Record<string, any> = await formik.validateForm();
|
||||
|
||||
if (statusToUse !== 'draft') {
|
||||
if (!formik.values.category) {
|
||||
errors.category = 'Category is required for review or publishing';
|
||||
}
|
||||
if (Array.isArray(filteredAttributesList) && filteredAttributesList.length > 0) {
|
||||
filteredAttributesList.forEach((attr: any) => {
|
||||
if (attr.is_required || attr.isRequired) {
|
||||
const val = formik.values.attributes?.[attr.code];
|
||||
if (val === undefined || val === null || val === '') {
|
||||
if (!errors.attributes) errors.attributes = {};
|
||||
(errors.attributes as Record<string, string>)[attr.code] = `${attr.name || attr.code} is required`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Handled by dynamic validation schema automatically
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
formik.setErrors(errors);
|
||||
@@ -1440,7 +1558,10 @@ export default function NewProduct() {
|
||||
<Select
|
||||
name="brand"
|
||||
value={formik.values.brand}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.setFieldTouched('brand', true, false);
|
||||
}}
|
||||
onBlur={formik.handleBlur}
|
||||
disabled={isReadOnlyView}
|
||||
>
|
||||
@@ -1471,7 +1592,10 @@ export default function NewProduct() {
|
||||
<Select
|
||||
name="unit"
|
||||
value={formik.values.unit}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.setFieldTouched('unit', true, false);
|
||||
}}
|
||||
onBlur={formik.handleBlur}
|
||||
disabled={isReadOnlyView}
|
||||
>
|
||||
@@ -1507,6 +1631,7 @@ export default function NewProduct() {
|
||||
formik.setFieldValue('category', catId, true);
|
||||
formik.setFieldTouched('category', true, false);
|
||||
}}
|
||||
onBlur={() => formik.setFieldTouched('category', true, true)}
|
||||
placeholder="Select Category..."
|
||||
error={formik.touched.category && typeof formik.errors.category === 'string' ? formik.errors.category : undefined}
|
||||
disabled={isReadOnlyView}
|
||||
@@ -1767,7 +1892,11 @@ export default function NewProduct() {
|
||||
values={formik.values.attributes || {}}
|
||||
errors={formik.errors.attributes as any}
|
||||
touched={formik.touched.attributes as any}
|
||||
onAttributeChange={(code, val) => formik.setFieldValue(`attributes.${code}`, val)}
|
||||
onAttributeChange={(code, val) => {
|
||||
formik.setFieldValue(`attributes.${code}`, val, true);
|
||||
formik.setFieldTouched(`attributes.${code}`, true, false);
|
||||
}}
|
||||
onAttributeBlur={(code) => formik.setFieldTouched(`attributes.${code}`, true)}
|
||||
onAddAttributeClick={(group) => handleOpenCreateAttributeModal(group.id || group._id)}
|
||||
readOnly={isReadOnlyView}
|
||||
/>
|
||||
@@ -1962,21 +2091,75 @@ export default function NewProduct() {
|
||||
<button
|
||||
type="button"
|
||||
disabled={isNextDisabled}
|
||||
onClick={() => {
|
||||
formik.validateForm().then((errors) => {
|
||||
onClick={async () => {
|
||||
const errors: any = await formik.validateForm();
|
||||
|
||||
if (activeTab === 'general') {
|
||||
const requiredFields = ['name', 'brand', 'unit', 'category'];
|
||||
const hasRequiredErrors = Object.keys(errors).some(k => requiredFields.includes(k));
|
||||
if (!hasRequiredErrors) {
|
||||
setActiveTab(currentTabs[currentTabs.findIndex(t => t.id === activeTab) + 1].id);
|
||||
} else {
|
||||
formik.setTouched(
|
||||
Object.keys(errors).reduce((acc: any, key: string) => {
|
||||
const touchedObj: any = { ...formik.touched };
|
||||
requiredFields.forEach(k => {
|
||||
if (errors[k]) touchedObj[k] = true;
|
||||
});
|
||||
formik.setTouched(touchedObj);
|
||||
}
|
||||
} else if (activeTab === 'attributes') {
|
||||
const requiredFields = ['name', 'brand', 'unit', 'category'];
|
||||
const hasRequiredErrors = Object.keys(errors).some(k => requiredFields.includes(k));
|
||||
const hasAttributeErrors = errors.attributes && Object.keys(errors.attributes).length > 0;
|
||||
|
||||
let isAttributesComplete = true;
|
||||
if (Array.isArray(filteredAttributesList)) {
|
||||
isAttributesComplete = !filteredAttributesList.some((attr: any) => {
|
||||
const isReq = attr.is_required === true || attr.isRequired === true;
|
||||
if (isReq) {
|
||||
const val = formik.values.attributes?.[attr.code];
|
||||
return isEmptyAttributeValue(val);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasRequiredErrors && !hasAttributeErrors && isAttributesComplete) {
|
||||
setActiveTab(currentTabs[currentTabs.findIndex(t => t.id === activeTab) + 1].id);
|
||||
} else {
|
||||
const touchedObj: any = {
|
||||
...formik.touched,
|
||||
...Object.keys(errors).reduce((acc: any, key: string) => {
|
||||
acc[key] = true;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
};
|
||||
if (errors.attributes) {
|
||||
touchedObj.attributes = {
|
||||
...formik.touched.attributes,
|
||||
...Object.keys(errors.attributes).reduce((acc: any, key: string) => {
|
||||
acc[key] = true;
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
if (Array.isArray(filteredAttributesList)) {
|
||||
const attributesTouch = touchedObj.attributes || {};
|
||||
filteredAttributesList.forEach((attr: any) => {
|
||||
const isReq = attr.is_required === true || attr.isRequired === true;
|
||||
if (isReq) {
|
||||
const val = formik.values.attributes?.[attr.code];
|
||||
if (isEmptyAttributeValue(val)) {
|
||||
attributesTouch[attr.code] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
touchedObj.attributes = attributesTouch;
|
||||
}
|
||||
formik.setTouched(touchedObj);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setActiveTab(currentTabs[currentTabs.findIndex(t => t.id === activeTab) + 1].id);
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user