Fix validation again I hope?

This commit is contained in:
2025-03-08 12:23:43 -05:00
parent c96f514bcd
commit 45fa583ce8
3 changed files with 175 additions and 86 deletions

View File

@@ -141,21 +141,33 @@ const ItemNumberCell = React.memo(({
field: Field<string>, field: Field<string>,
onChange: (value: any) => void onChange: (value: any) => void
}) => { }) => {
// Determine if the field has an error // Helper function to check if a value is empty
const hasError = errors.some(error => error.level === 'error' || error.level === 'warning'); const isEmpty = (val: any): boolean =>
val === undefined ||
val === null ||
val === '' ||
(Array.isArray(val) && val.length === 0) ||
(typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0);
// If we have a value or itemNumber, ignore "required" errors
const displayValue = itemNumber || value;
const filteredErrors = !isEmpty(displayValue)
? errors.filter(error => !error.message?.toLowerCase().includes('required'))
: errors;
// Determine if the field has an error after filtering
const hasError = filteredErrors.some(error => error.level === 'error' || error.level === 'warning');
// Determine if the field is required but empty // Determine if the field is required but empty
const isRequiredButEmpty = errors.some(error => const isRequiredButEmpty = isEmpty(displayValue) &&
error.message?.toLowerCase().includes('required') && errors.some(error => error.message?.toLowerCase().includes('required'));
(!value || (typeof value === 'string' && value.trim() === ''))
);
// Only show error icons for non-empty fields // Only show error icons for non-empty fields with actual errors (not just required errors)
const shouldShowErrorIcon = hasError && value && (typeof value !== 'string' || value.trim() !== ''); const shouldShowErrorIcon = hasError && !isEmpty(displayValue);
// Get error messages for the tooltip // Get error messages for the tooltip
const errorMessages = shouldShowErrorIcon const errorMessages = shouldShowErrorIcon
? errors.filter(e => e.level === 'error' || e.level === 'warning').map(e => e.message).join('\n') ? filteredErrors.filter(e => e.level === 'error' || e.level === 'warning').map(e => e.message).join('\n')
: ''; : '';
return ( return (
@@ -164,13 +176,13 @@ const ItemNumberCell = React.memo(({
{isValidating ? ( {isValidating ? (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-blue-500" /> <Loader2 className="h-4 w-4 animate-spin text-blue-500" />
<span>{itemNumber || value || ''}</span> <span>{displayValue || ''}</span>
</div> </div>
) : ( ) : (
<div className="truncate overflow-hidden"> <div className="truncate overflow-hidden">
<BaseCellContent <BaseCellContent
field={field} field={field}
value={itemNumber || value} value={displayValue}
onChange={onChange} onChange={onChange}
hasErrors={hasError || isRequiredButEmpty} hasErrors={hasError || isRequiredButEmpty}
options={[]} options={[]}
@@ -222,26 +234,32 @@ const ValidationCell = ({
); );
} }
// Determine if the field has an error // Helper function to check if a value is empty
const hasError = errors.some(error => error.level === 'error' || error.level === 'warning'); const isEmpty = (val: any): boolean =>
val === undefined ||
val === null ||
val === '' ||
(Array.isArray(val) && val.length === 0) ||
(typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0);
// If we have a value, ignore "required" errors
const filteredErrors = !isEmpty(value)
? errors.filter(error => !error.message?.toLowerCase().includes('required'))
: errors;
// Determine if the field has an error after filtering
const hasError = filteredErrors.some(error => error.level === 'error' || error.level === 'warning');
// Determine if the field is required but empty // Determine if the field is required but empty
const isRequiredButEmpty = errors.some(error => const isRequiredButEmpty = isEmpty(value) &&
error.message?.toLowerCase().includes('required') && errors.some(error => error.message?.toLowerCase().includes('required'));
(value === undefined || value === null ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0))
);
// Only show error icons for non-empty fields // Only show error icons for non-empty fields with actual errors (not just required errors)
const shouldShowErrorIcon = hasError && const shouldShowErrorIcon = hasError && !isEmpty(value);
!(value === undefined || value === null ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0));
// Get error messages for the tooltip // Get error messages for the tooltip
const errorMessages = shouldShowErrorIcon const errorMessages = shouldShowErrorIcon
? errors.filter(e => e.level === 'error' || e.level === 'warning').map(e => e.message).join('\n') ? filteredErrors.filter(e => e.level === 'error' || e.level === 'warning').map(e => e.message).join('\n')
: ''; : '';
// Check if this is a multiline field // Check if this is a multiline field
@@ -249,24 +267,30 @@ const ValidationCell = ({
(field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') && (field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') &&
field.fieldType.multiline === true; field.fieldType.multiline === true;
// Adjust cell height for multiline fields // Check for price field
const cellHeight = isMultiline ? 'min-h-[80px]' : 'h-10'; const isPrice = typeof field.fieldType === 'object' &&
(field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') &&
field.fieldType.price === true;
return ( return (
<TableCell className="p-1" style={{ width: `${width}px`, minWidth: `${width}px` }}> <TableCell className="p-1" style={{ width: `${width}px`, minWidth: `${width}px` }}>
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''} ${cellHeight}`}> <div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
{isValidating ? (
<div className="flex items-center justify-center gap-2">
<div className={`truncate overflow-hidden ${isMultiline ? 'h-full' : ''}`}> <Loader2 className="h-4 w-4 animate-spin text-blue-500" />
<BaseCellContent <span>Loading...</span>
field={field} </div>
value={value} ) : (
onChange={onChange} <div className="truncate overflow-hidden">
hasErrors={hasError || isRequiredButEmpty} <BaseCellContent
options={options} field={field}
/> value={value}
</div> onChange={onChange}
hasErrors={hasError || isRequiredButEmpty}
options={options}
/>
</div>
)}
{shouldShowErrorIcon && ( {shouldShowErrorIcon && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-20"> <div className="absolute right-2 top-1/2 -translate-y-1/2 z-20">
<ValidationIcon error={{ <ValidationIcon error={{

View File

@@ -170,6 +170,9 @@ const ValidationContainer = <T extends string>({
return { success: false }; return { success: false };
} }
// Add logging to help debug
console.log(`Validating UPC for row ${rowIndex}. Supplier ID: ${supplierId}, UPC: ${upcValue}`);
// Check if we've already validated this UPC/supplier combination // Check if we've already validated this UPC/supplier combination
const cacheKey = `${supplierId}-${upcValue}`; const cacheKey = `${supplierId}-${upcValue}`;
if (processedUpcMapRef.current.has(cacheKey)) { if (processedUpcMapRef.current.has(cacheKey)) {
@@ -188,7 +191,7 @@ const ValidationContainer = <T extends string>({
return { success: false }; return { success: false };
} }
// Make API call to validate UPC // Make API call to validate UPC - ensure we're using supplierId (not company)
const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}`); const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}`);
// Process the response // Process the response
@@ -372,8 +375,35 @@ const ValidationContainer = <T extends string>({
top: window.scrollY top: window.scrollY
}; };
// Update the main data state // Find the original index in the data array
updateRow(rowIndex, fieldKey, processedValue); const rowData = filteredData[rowIndex];
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
if (originalIndex === -1) {
// If we can't find the original row, just do a simple update
updateRow(rowIndex, fieldKey, processedValue);
} else {
// Create a new row with the updated field
const updatedRow = {
...data[originalIndex],
[fieldKey]: processedValue
};
// Clear any validation errors for this field
if (updatedRow.__errors && updatedRow.__errors[String(fieldKey)]) {
const updatedErrors = { ...updatedRow.__errors };
delete updatedErrors[String(fieldKey)];
updatedRow.__errors = Object.keys(updatedErrors).length > 0 ? updatedErrors : undefined;
}
// Update the data directly
setData(prevData => {
const newData = [...prevData];
newData[originalIndex] = updatedRow;
return newData;
});
}
// Restore scroll position after update // Restore scroll position after update
setTimeout(() => { setTimeout(() => {
@@ -381,15 +411,9 @@ const ValidationContainer = <T extends string>({
}, 0); }, 0);
// Now handle any additional logic for specific fields // Now handle any additional logic for specific fields
const rowData = filteredData[rowIndex];
// If updating company field, fetch product lines
if (fieldKey === 'company' && value) { if (fieldKey === 'company' && value) {
// Clear any existing line/subline values for this row if company changes // Clear any existing line/subline values for this row if company changes
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
if (originalIndex !== -1) { if (originalIndex !== -1) {
// Update the data to clear line and subline
setData(prevData => { setData(prevData => {
const newData = [...prevData]; const newData = [...prevData];
newData[originalIndex] = { newData[originalIndex] = {
@@ -405,45 +429,43 @@ const ValidationContainer = <T extends string>({
if (rowData && rowData.__index) { if (rowData && rowData.__index) {
await fetchProductLines(rowData.__index, value.toString()); await fetchProductLines(rowData.__index, value.toString());
} }
}
// If company field is being updated AND there's a UPC value, validate UPC
if (rowData) { // If updating supplier field AND there's a UPC value, validate UPC
const rowDataAny = rowData as Record<string, any>; if (fieldKey === 'supplier' && value && rowData) {
if (rowDataAny.upc || rowDataAny.barcode) { const rowDataAny = rowData as Record<string, any>;
const upcValue = rowDataAny.upc || rowDataAny.barcode; if (rowDataAny.upc || rowDataAny.barcode) {
const upcValue = rowDataAny.upc || rowDataAny.barcode;
// Mark this row as being validated
setValidatingUpcRows(prev => { // Mark this row as being validated
const newSet = new Set(prev); setValidatingUpcRows(prev => {
newSet.add(rowIndex); const newSet = new Set(prev);
return newSet; newSet.add(rowIndex);
}); return newSet;
});
// Set global validation state
setIsValidatingUpc(true); // Set global validation state
setIsValidatingUpc(true);
await validateUpc(rowIndex, value.toString(), upcValue.toString());
// Use supplier ID (the value being set) to validate UPC
// Update validation state await validateUpc(rowIndex, value.toString(), upcValue.toString());
setValidatingUpcRows(prev => {
const newSet = new Set(prev); // Update validation state
newSet.delete(rowIndex); setValidatingUpcRows(prev => {
if (newSet.size === 0) { const newSet = new Set(prev);
setIsValidatingUpc(false); newSet.delete(rowIndex);
} if (newSet.size === 0) {
return newSet; setIsValidatingUpc(false);
}); }
} return newSet;
});
} }
} }
// If updating line field, fetch sublines // If updating line field, fetch sublines
if (fieldKey === 'line' && value) { if (fieldKey === 'line' && value) {
// Clear any existing subline value for this row // Clear any existing subline value for this row
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
if (originalIndex !== -1) { if (originalIndex !== -1) {
// Update the data to clear subline only
setData(prevData => { setData(prevData => {
const newData = [...prevData]; const newData = [...prevData];
newData[originalIndex] = { newData[originalIndex] = {
@@ -474,6 +496,7 @@ const ValidationContainer = <T extends string>({
// Set global validation state // Set global validation state
setIsValidatingUpc(true); setIsValidatingUpc(true);
// Use supplier ID from the row data (NOT company ID) to validate UPC
await validateUpc(rowIndex, rowDataAny.supplier.toString(), value.toString()); await validateUpc(rowIndex, rowDataAny.supplier.toString(), value.toString());
// Update validation state // Update validation state

View File

@@ -32,7 +32,15 @@ export const useValidation = <T extends string>(
field.validations.forEach(validation => { field.validations.forEach(validation => {
switch (validation.rule) { switch (validation.rule) {
case 'required': case 'required':
if (value === undefined || value === null || value === '') { // More granular check for empty values
const isEmpty =
value === undefined ||
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && Object.keys(value).length === 0);
if (isEmpty) {
errors.push({ errors.push({
message: validation.errorMessage || 'This field is required', message: validation.errorMessage || 'This field is required',
level: validation.level || 'error' level: validation.level || 'error'
@@ -74,6 +82,14 @@ export const useValidation = <T extends string>(
// Run field-level validations // Run field-level validations
const fieldErrors: Record<string, ValidationError[]> = {} const fieldErrors: Record<string, ValidationError[]> = {}
// Helper function to check if a value is empty
const isEmpty = (value: any): boolean =>
value === undefined ||
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
fields.forEach(field => { fields.forEach(field => {
const value = row[String(field.key) as keyof typeof row] const value = row[String(field.key) as keyof typeof row]
const errors = validateField(value, field as Field<T>) const errors = validateField(value, field as Field<T>)
@@ -83,15 +99,15 @@ export const useValidation = <T extends string>(
} }
}) })
// Special validation for supplier and company fields // Special validation for supplier and company fields - only apply if the field exists in fields
if (fields.some(field => String(field.key) === 'supplier') && !row.supplier) { if (fields.some(field => String(field.key) === 'supplier') && isEmpty(row.supplier)) {
fieldErrors['supplier'] = [{ fieldErrors['supplier'] = [{
message: 'Supplier is required', message: 'Supplier is required',
level: 'error' level: 'error'
}] }]
} }
if (fields.some(field => String(field.key) === 'company') && !row.company) { if (fields.some(field => String(field.key) === 'company') && isEmpty(row.company)) {
fieldErrors['company'] = [{ fieldErrors['company'] = [{
message: 'Company is required', message: 'Company is required',
level: 'error' level: 'error'
@@ -249,6 +265,14 @@ export const useValidation = <T extends string>(
// Run complete validation // Run complete validation
const validateData = useCallback(async (data: RowData<T>[]) => { const validateData = useCallback(async (data: RowData<T>[]) => {
// Helper function to check if a value is empty
const isEmpty = (value: any): boolean =>
value === undefined ||
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
// Step 1: Run field and row validation // Step 1: Run field and row validation
const rowValidations = await Promise.all( const rowValidations = await Promise.all(
data.map((row, index) => validateRow(row, index, data)) data.map((row, index) => validateRow(row, index, data))
@@ -276,7 +300,25 @@ export const useValidation = <T extends string>(
...(tableValidation.__errors || {}) ...(tableValidation.__errors || {})
} }
newRow.__errors = combinedErrors // Filter out "required" errors for fields that have values
const filteredErrors: Record<string, InfoWithSource> = {}
Object.entries(combinedErrors).forEach(([key, error]) => {
const fieldValue = row[key as keyof typeof row]
// If the field has a value and the only error is "required", skip it
if (!isEmpty(fieldValue) &&
error &&
typeof error === 'object' &&
'message' in error &&
error.message?.toLowerCase().includes('required')) {
return
}
filteredErrors[key] = error as InfoWithSource
})
newRow.__errors = Object.keys(filteredErrors).length > 0 ? filteredErrors : undefined
return newRow return newRow
}) })