From 7cd9ab5e0c89ed99e0c8232b211264181c626ecc Mon Sep 17 00:00:00 2001 From: Mahir-Mohamed Date: Tue, 11 Aug 2026 18:19:41 +0530 Subject: [PATCH 1/3] fix: improve product form validation and navigation guards --- src/components/customs/CategoryTreeSelect.tsx | 31 ++- src/components/customs/Select.tsx | 3 +- .../components/DynamicAttributeRenderer.tsx | 11 +- .../components/DynamicAttributesSection.tsx | 3 + .../components/ProductAttributeGroup.tsx | 3 + src/features/product/pages/NewProduct.tsx | 245 +++++++++++++++--- 6 files changed, 250 insertions(+), 46 deletions(-) diff --git a/src/components/customs/CategoryTreeSelect.tsx b/src/components/customs/CategoryTreeSelect.tsx index 8fe1016..bd16b4d 100644 --- a/src/components/customs/CategoryTreeSelect.tsx +++ b/src/components/customs/CategoryTreeSelect.tsx @@ -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 = ({ value, onChange, + onBlur, placeholder = 'Select Category...', error, disabled = false, @@ -68,7 +70,7 @@ export const CategoryTreeSelect: React.FC = ({ const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(''); const [expandedNodes, setExpandedNodes] = useState>(new Set()); - + // Quick Create Drawer state const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [newCatName, setNewCatName] = useState(''); @@ -86,12 +88,17 @@ export const CategoryTreeSelect: React.FC = ({ 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 = ({ 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 = ({
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` }} >
@@ -233,11 +239,10 @@ export const CategoryTreeSelect: React.FC = ({ {/* Trigger Button */}
!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' + }`} >
diff --git a/src/components/customs/Select.tsx b/src/components/customs/Select.tsx index 358848d..8c6883e 100644 --- a/src/components/customs/Select.tsx +++ b/src/components/customs/Select.tsx @@ -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; diff --git a/src/features/product/components/DynamicAttributeRenderer.tsx b/src/features/product/components/DynamicAttributeRenderer.tsx index 7557beb..c2fb84b 100644 --- a/src/features/product/components/DynamicAttributeRenderer.tsx +++ b/src/features/product/components/DynamicAttributeRenderer.tsx @@ -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 = attribute, value, onChange, + onBlur, error, touched, readOnly, @@ -47,6 +49,7 @@ export const DynamicAttributeRenderer: React.FC =