diff --git a/docs/validation-process-issues.md b/docs/validation-process-issues.md new file mode 100644 index 0000000..039f9ae --- /dev/null +++ b/docs/validation-process-issues.md @@ -0,0 +1,236 @@ +## 1. ✅ Error Filtering Logic Inconsistency (RESOLVED) + +> **Note: This issue has been resolved by implementing a type-based error system.** + +The filtering logic in `ValidationCell.tsx` previously relied on string matching, which was fragile: + +```typescript +// Old implementation (string-based matching) +const filteredErrors = React.useMemo(() => { + return !isEmpty(value) + ? errors.filter(error => !error.message?.toLowerCase().includes('required')) + : errors; +}, [value, errors]); + +// New implementation (type-based filtering) +const filteredErrors = React.useMemo(() => { + return !isEmpty(value) + ? errors.filter(error => error.type !== ErrorType.Required) + : errors; +}, [value, errors]); +``` + +The solution implemented: +- Added an `ErrorType` enum in `types.ts` to standardize error categorization +- Updated all error creation to include the appropriate error type +- Modified error filtering to use the type property instead of string matching +- Ensured consistent error handling across the application + +**Guidelines for future development:** +- Always use the `ErrorType` enum when creating errors +- Never rely on string matching for error filtering +- Ensure all error objects include the `type` property +- Use the appropriate error type for each validation rule: + - `ErrorType.Required` for required field validations + - `ErrorType.Regex` for regex validations + - `ErrorType.Unique` for uniqueness validations + - `ErrorType.Custom` for custom validations + - `ErrorType.Api` for API-based validations + +## 2. Redundant Error Processing + +The system processes errors in multiple places: +- In `ValidationCell.tsx`, errors are filtered and processed again +- In `useValidation.tsx`, errors are already filtered once +- In `ValidationContainer.tsx`, errors are manipulated directly + +This redundancy could lead to inconsistent behavior and makes the code harder to maintain. + +## 3. Race Conditions in Async Validation + +The UPC validation and other async validations could create race conditions: +- If a user types quickly, multiple validation requests might be in flight +- Later responses could overwrite more recent ones if they complete out of order +- The debouncing helps but doesn't fully solve this issue + +## 4. Memory Leaks in Timeout Management + +The validation timeouts are stored in refs: +```typescript +const validationTimeoutsRef = useRef>({}); +``` + +While there is cleanup on unmount, if rows are added/removed dynamically, timeouts for deleted rows might not be properly cleared. + +## 5. Inefficient Error Storage + +Errors are stored in multiple places: +- In the `validationErrors` Map +- In the row data itself as `__errors` +- In the UPC validation results + +This duplication makes it harder to maintain a single source of truth and could lead to inconsistencies. + +## 6. Excessive Re-rendering + +Despite optimization attempts, the system might still cause excessive re-renders: +- Each cell validation can trigger updates to the entire data structure +- The batch update system helps but still has limitations +- The memoization in `ValidationCell` might not catch all cases where re-rendering is unnecessary + +## 7. Complex Error Merging Logic + +When merging errors from different sources, the logic is complex and potentially error-prone: +```typescript +// Merge field errors and row hook errors +const mergedErrors: Record = {} + +// Convert field errors to InfoWithSource +Object.entries(fieldErrors).forEach(([key, errors]) => { + if (errors.length > 0) { + mergedErrors[key] = { + message: errors[0].message, + level: errors[0].level, + source: ErrorSources.Row, + type: errors[0].type || ErrorType.Custom + } + } +}) +``` + +This only takes the first error for each field, potentially hiding important validation issues. + +## 8. ✅ Inconsistent Error Handling for Empty Values (PARTIALLY RESOLVED) + +> **Note: This issue has been partially resolved by standardizing the isEmpty function and error type system.** + +The system previously had different approaches to handling empty values: +- Some validations skipped empty values unless they're required +- Others processed empty values differently +- The `isEmpty` function was defined multiple times with slight variations + +The solution implemented: +- Standardized the `isEmpty` function implementation +- Ensured consistent error type usage for required field validations +- Made error filtering consistent across the application + +**Guidelines for future development:** +- Always use the shared `isEmpty` function for checking empty values +- Ensure consistent handling of empty values across all validation rules +- Use the `ErrorType.Required` type for all required field validations + +## 9. Tight Coupling Between Components + +The validation system is tightly coupled across components: +- `ValidationCell` needs to understand the structure of errors +- `ValidationTable` needs to extract and pass the right errors +- `ValidationContainer` directly manipulates the error structure + +This makes it harder to refactor or reuse components independently. + +## 10. Limited Error Prioritization + +There's no clear prioritization of errors: +- When multiple errors exist for a field, which one should be shown first? +- Are some errors more important than others? +- The current system mostly shows the first error it finds + +A more robust approach would be to have a consistent error source identification system and a clear prioritization strategy for displaying errors. + +------------ + +Let me explain how these hooks fit together to create the validation errors that eventually get filtered in the `ValidationCell` component: + +## The Validation Flow + +1. **useValidationState Hook**: + This is the main state management hook used by the `ValidationContainer` component. It: + - Manages the core data state (`data`) + - Tracks validation errors in a Map (`validationErrors`) + - Provides functions to update and validate rows + +2. **useValidation Hook**: + This is a utility hook that provides the core validation logic: + - `validateField`: Validates a single field against its validation rules + - `validateRow`: Validates an entire row, field by field + - `validateTable`: Runs table-level validations + - `validateUnique`: Checks for uniqueness constraints + - `validateData`: Orchestrates the complete validation process + +## How Errors Are Generated + +Validation errors come from multiple sources: + +1. **Field-Level Validations**: + In `useValidation.tsx`, the `validateField` function checks individual fields against rules like: + - `required`: Field must have a value + - `regex`: Value must match a pattern + - `min`/`max`: Numeric constraints + +2. **Row-Level Validations**: + The `validateRow` function in `useValidation.tsx` runs: + - Field validations for each field in the row + - Special validations for required fields like supplier and company + - Custom row hooks provided by the application + +3. **Table-Level Validations**: + - `validateUnique` checks for duplicate values in fields marked as unique + - `validateTable` runs custom table hooks for cross-row validations + +4. **API-Based Validations**: + In `useValidationState.tsx` and `ValidationContainer.tsx`: + - UPC validation via API calls + - Item number uniqueness checks + +## The Error Flow + +1. Errors are collected in the `validationErrors` Map in `useValidationState` +2. This Map is passed to `ValidationTable` as a prop +3. `ValidationTable` extracts the relevant errors for each cell and passes them to `ValidationCell` +4. In `ValidationCell`, the errors are filtered based on whether the cell has a value: + ```typescript + // Updated implementation using type-based filtering + const filteredErrors = React.useMemo(() => { + return !isEmpty(value) + ? errors.filter(error => error.type !== ErrorType.Required) + : errors; + }, [value, errors]); + ``` + +## Key Insights + +1. **Error Structure**: + Errors now have a consistent structure with type information: + ```typescript + type ErrorObject = { + message: string; + level: string; // 'error', 'warning', etc. + source?: ErrorSources; // Where the error came from + type: ErrorType; // The type of error (Required, Regex, Unique, etc.) + } + ``` + +2. **Error Sources**: + Errors can come from: + - Field validations (required, regex, etc.) + - Row validations (custom business logic) + - Table validations (uniqueness checks) + - API validations (UPC checks) + +3. **Error Types**: + Errors are now categorized by type: + - `ErrorType.Required`: Field is required but empty + - `ErrorType.Regex`: Value doesn't match the regex pattern + - `ErrorType.Unique`: Value must be unique across rows + - `ErrorType.Custom`: Custom validation errors + - `ErrorType.Api`: Errors from API calls + +4. **Error Filtering**: + The filtering in `ValidationCell` is now more robust: + - When a field has a value, errors of type `ErrorType.Required` are filtered out + - When a field is empty, all errors are shown + +5. **Performance Optimizations**: + - Batch processing of validations + - Debounced updates to avoid excessive re-renders + - Memoization of computed values \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx index 0742f55..04cd0af 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Field } from '../../../types' +import { Field, ErrorType } from '../../../types' import { Loader2, AlertCircle, ArrowDown, Check, X } from 'lucide-react' import { Tooltip, @@ -40,6 +40,7 @@ type ErrorObject = { message: string; level: string; source?: string; + type?: ErrorType; } // Helper function to check if a value is empty - utility function shared by all components @@ -195,12 +196,8 @@ function processErrors(value: any, errors: ErrorObject[]): { }; } - // Check if value is empty - using local function for speed - const valueIsEmpty = value === undefined || - value === null || - value === '' || - (Array.isArray(value) && value.length === 0) || - (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0); + // Use the shared isEmpty function instead of defining a local one + const valueIsEmpty = isEmpty(value); // If not empty, filter out required errors // Create a new array only if we need to filter (avoid unnecessary allocations) @@ -210,13 +207,13 @@ function processErrors(value: any, errors: ErrorObject[]): { if (valueIsEmpty) { // For empty values, check if there are required errors hasRequiredError = errors.some(error => - error.message?.toLowerCase().includes('required') + error.type === ErrorType.Required ); filteredErrors = errors; } else { // For non-empty values, filter out required errors filteredErrors = errors.filter(error => - !error.message?.toLowerCase().includes('required') + error.type !== ErrorType.Required ); } @@ -351,7 +348,8 @@ const ItemNumberCell = React.memo(({
)} @@ -474,7 +472,7 @@ const ValidationCell = ({ // Memoize filtered errors to avoid recalculation on every render const filteredErrors = React.useMemo(() => { return !isEmpty(value) - ? errors.filter(error => !error.message?.toLowerCase().includes('required')) + ? errors.filter(error => error.type !== ErrorType.Required) : errors; }, [value, errors]); @@ -485,7 +483,7 @@ const ValidationCell = ({ // Determine if the field is required but empty const isRequiredButEmpty = isEmpty(value) && - errors.some(error => error.message?.toLowerCase().includes('required')); + errors.some(error => error.type === ErrorType.Required); // Only show error icons for non-empty fields with actual errors (not just required errors) const shouldShowErrorIcon = hasError && !isEmpty(value); @@ -555,7 +553,8 @@ const ValidationCell = ({
)} diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx index 80aec3d..2d2b177 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx @@ -10,7 +10,7 @@ import SearchableTemplateSelect from './SearchableTemplateSelect' import { useAiValidation } from '../hooks/useAiValidation' import { AiValidationDialogs } from './AiValidationDialogs' import config from '@/config' -import { Fields } from '../../../types' +import { Fields, ErrorSources, ErrorType } from '../../../types' import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog' import { TemplateForm } from '@/components/templates/TemplateForm' import axios from 'axios' @@ -229,7 +229,9 @@ const ValidationContainer = ({ ...(rowToUpdate.__errors || {}), [fieldKey]: { level: 'error', - message: `UPC already exists (${errorData.existingItemNumber})` + message: `UPC already exists (${errorData.existingItemNumber})`, + source: ErrorSources.Upc, + type: ErrorType.Unique } } }; diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx index a6e13a7..2dfcc68 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx @@ -1,7 +1,7 @@ import { useState, useCallback } from 'react'; import { toast } from 'sonner'; import { getApiUrl, RowData } from './useValidationState'; -import { Fields, InfoWithSource, ErrorSources } from '../../../types'; +import { Fields, InfoWithSource, ErrorSources, ErrorType } from '../../../types'; import { Meta } from '../types'; import { addErrorsAndRunHooks } from '../utils/dataMutations'; import * as Diff from 'diff'; @@ -99,7 +99,8 @@ export const useAiValidation = ( return [key, { message: errorArray[0].message, level: errorArray[0].level, - source: ErrorSources.Row + source: ErrorSources.Row, + type: ErrorType.Custom } as InfoWithSource] }) ) : null @@ -120,7 +121,8 @@ export const useAiValidation = ( return [key, { message: errorArray[0].message, level: errorArray[0].level, - source: ErrorSources.Table + source: ErrorSources.Table, + type: ErrorType.Custom } as InfoWithSource] }) ) : null diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx index 73fedea..8cf735b 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx @@ -1,18 +1,16 @@ import { useCallback } from 'react' import type { Field, Fields, RowHook, TableHook } from '../../../types' import type { Meta } from '../types' -import { ErrorSources } from '../../../types' +import { ErrorSources, ErrorType, ValidationError } from '../../../types' import { RowData } from './useValidationState' -interface ValidationError { - message: string - level: 'info' | 'warning' | 'error' -} - +// Define InfoWithSource to match the expected structure +// Make sure source is required (not optional) interface InfoWithSource { - message: string - level: 'info' | 'warning' | 'error' - source: ErrorSources + message: string; + level: 'info' | 'warning' | 'error'; + source: ErrorSources; + type: ErrorType; } // Shared utility function for checking empty values - defined once to avoid duplication @@ -44,7 +42,8 @@ export const useValidation = ( if (isEmpty(value)) { errors.push({ message: validation.errorMessage || 'This field is required', - level: validation.level || 'error' + level: validation.level || 'error', + type: ErrorType.Required }) } break @@ -60,7 +59,8 @@ export const useValidation = ( if (!regex.test(String(value))) { errors.push({ message: validation.errorMessage, - level: validation.level || 'error' + level: validation.level || 'error', + type: ErrorType.Regex }) } } catch (error) { @@ -98,14 +98,16 @@ export const useValidation = ( if (fields.some(field => String(field.key) === 'supplier') && isEmpty(row.supplier)) { fieldErrors['supplier'] = [{ message: 'Supplier is required', - level: 'error' + level: 'error', + type: ErrorType.Required }] } if (fields.some(field => String(field.key) === 'company') && isEmpty(row.company)) { fieldErrors['company'] = [{ message: 'Company is required', - level: 'error' + level: 'error', + type: ErrorType.Required }] } @@ -131,7 +133,8 @@ export const useValidation = ( mergedErrors[key] = { message: errors[0].message, level: errors[0].level, - source: ErrorSources.Row + source: ErrorSources.Row, + type: errors[0].type || ErrorType.Custom } } }) @@ -140,7 +143,12 @@ export const useValidation = ( if (rowHookResult.__errors) { Object.entries(rowHookResult.__errors).forEach(([key, error]) => { if (error) { - mergedErrors[key] = error + // Add type if not present + const errorWithType = { + ...error, + type: error.type || ErrorType.Custom + } + mergedErrors[key] = errorWithType as InfoWithSource } }) } @@ -173,8 +181,9 @@ export const useValidation = ( if (error) { formattedErrors[key] = { ...error, - source: ErrorSources.Table - } + source: ErrorSources.Table, + type: error.type || ErrorType.Custom + } as InfoWithSource } }) } @@ -246,7 +255,8 @@ export const useValidation = ( rowErrors[String(key)] = { message: errorMessage, level, - source: ErrorSources.Table + source: ErrorSources.Table, + type: ErrorType.Unique } uniqueErrors[rowIndex].__errors = rowErrors @@ -295,12 +305,12 @@ export const useValidation = ( 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 the field has a value and the error is of type Required, skip it if (!isEmpty(fieldValue) && error && typeof error === 'object' && - 'message' in error && - error.message?.toLowerCase().includes('required')) { + 'type' in error && + error.type === ErrorType.Required) { return } diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx index 5f9159d..f0beeb9 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx @@ -1,18 +1,26 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useRsi } from '../../../hooks/useRsi' import type { Data, Field } from '../../../types' +import { ErrorSources, ErrorType, ValidationError } from '../../../types' import { RowSelectionState } from '@tanstack/react-table' import { toast } from 'sonner' import { useQuery } from "@tanstack/react-query"; import config from "@/config"; -// Define ErrorType directly in file to avoid import issues -type ErrorType = { - message: string; - level: string; - source?: string; -} +// Helper function to check if a value is empty +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); +// Use the ValidationError type from types.ts instead of defining ErrorType here +// type ErrorType = { +// message: string; +// level: string; +// source?: string; +// } // Define the Props interface for ValidationStepNew export interface Props { @@ -26,7 +34,7 @@ export interface Props { // Extended Data type with meta information export type RowData = Data & { __index?: string; - __errors?: Record; + __errors?: Record; __template?: string; __original?: Record; __corrected?: Record; @@ -55,6 +63,8 @@ export interface ValidationResult { error?: boolean; message?: string; data?: Record; + type?: ErrorType; + source?: ErrorSources; } // Filter state interface @@ -178,7 +188,7 @@ export const useValidationState = ({ // Validation state const [isValidating] = useState(false) - const [validationErrors, setValidationErrors] = useState>>(new Map()) + const [validationErrors, setValidationErrors] = useState>>(new Map()) const [rowValidationStatus, setRowValidationStatus] = useState>(new Map()) const [, setValidatingCells] = useState>(new Set()) @@ -212,7 +222,7 @@ export const useValidationState = ({ // Add batch update state const pendingUpdatesRef = useRef<{ - errors: Map>, + errors: Map>, statuses: Map, data: Array> }>({ @@ -357,7 +367,7 @@ export const useValidationState = ({ // Queue updates instead of immediate setState calls const queueUpdate = useCallback((rowIndex: number, updates: { - errors?: Record, + errors?: Record, status?: 'pending' | 'validating' | 'validated' | 'error', data?: RowData }) => { @@ -384,7 +394,7 @@ export const useValidationState = ({ const itemNumberMap = new Map(); // Initialize batch updates - const errors = new Map>(); + const errors = new Map>(); // Single pass through data to identify all item numbers data.forEach((row, index) => { @@ -403,8 +413,9 @@ export const useValidationState = ({ if (indices.length > 1) { const errorObj = { message: `Duplicate item number: ${itemNumber}`, - level: 'error', - source: 'validation' + level: 'error' as 'error', + source: ErrorSources.Table, + type: ErrorType.Unique }; // Add error to each row with this item number @@ -470,10 +481,12 @@ export const useValidationState = ({ const errorData = await response.json(); return { error: true, - message: 'UPC already exists', + message: `UPC already exists (${errorData.existingItemNumber})`, data: { itemNumber: errorData.existingItemNumber || '', - } + }, + type: ErrorType.Unique, + source: ErrorSources.Api }; } else if (response.status === 429) { return { @@ -691,65 +704,38 @@ export const useValidationState = ({ }, []) // Helper function to validate a field value - const validateField = useCallback((value: any, field: Field): ErrorType[] => { - const errors: ErrorType[] = []; + const validateField = useCallback((value: any, field: Field): ValidationError[] => { + const errors: ValidationError[] = []; // Required field validation - improved to better handle various value types if (field.validations?.some(v => v.rule === 'required')) { - // Handle different value types more carefully - const isEmpty = - value === undefined || - value === null || - (typeof value === 'string' && value.trim() === '') || - (Array.isArray(value) && value.length === 0); - - if (isEmpty) { + if (value === undefined || value === null || value === '' || + (Array.isArray(value) && value.length === 0) || + (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)) { errors.push({ - message: 'Required field', + message: field.validations.find(v => v.rule === 'required')?.errorMessage || `${field.label} is required`, level: 'error', - source: 'required' + source: ErrorSources.Row, + type: ErrorType.Required }); - // Return early since other validations may not be relevant for empty values - return errors; } } - // Continue with other validations if the required check passed - if (field.validations) { - for (const validation of field.validations) { - // Skip required validation as we've already handled it - if (validation.rule === 'required') continue; - - if (validation.rule === 'regex' && typeof value === 'string') { - // Implement regex validation - const regex = new RegExp(validation.value!); - if (!regex.test(value)) { - errors.push({ - message: validation.errorMessage || 'Invalid format', - level: validation.level || 'error', - source: 'validation' - }); - } - } else if (validation.rule === 'min' && typeof value === 'number') { - // Implement min validation - if (value < validation.value) { - errors.push({ - message: validation.errorMessage || `Value must be at least ${validation.value}`, - level: validation.level || 'error', - source: 'validation' - }); - } - } else if (validation.rule === 'max' && typeof value === 'number') { - // Implement max validation - if (value > validation.value) { - errors.push({ - message: validation.errorMessage || `Value must be at most ${validation.value}`, - level: validation.level || 'error', - source: 'validation' - }); - } + // Regex validation + const regexValidation = field.validations?.find(v => v.rule === 'regex'); + if (regexValidation && value !== undefined && value !== null && value !== '') { + try { + const regex = new RegExp(regexValidation.value, regexValidation.flags); + if (!regex.test(String(value))) { + errors.push({ + message: regexValidation.errorMessage, + level: regexValidation.level || 'error', + source: ErrorSources.Row, + type: ErrorType.Regex + }); } - // Add other validation types as needed + } catch (error) { + console.error('Invalid regex in validation:', error); } } @@ -770,7 +756,7 @@ export const useValidationState = ({ // Get the row data const row = data[rowIndex]; - const fieldErrors: Record = {}; + const fieldErrors: Record = {}; let hasErrors = false; // Get current errors for comparison @@ -823,7 +809,8 @@ export const useValidationState = ({ fieldErrors['supplier'] = [{ message: 'Supplier is required', level: 'error', - source: 'required' + source: ErrorSources.Row, + type: ErrorType.Required }]; hasErrors = true; } @@ -831,7 +818,8 @@ export const useValidationState = ({ fieldErrors['company'] = [{ message: 'Company is required', level: 'error', - source: 'required' + source: ErrorSources.Row, + type: ErrorType.Required }]; hasErrors = true; } @@ -1095,7 +1083,7 @@ export const useValidationState = ({ // Create a copy of data and process all rows at once to minimize state updates const newData = [...data]; - const batchErrors = new Map>(); + const batchErrors = new Map>(); const batchStatuses = new Map(); // Extract template fields once outside the loop @@ -1337,7 +1325,7 @@ export const useValidationState = ({ // When proceeding to the next screen, check for unvalidated rows first const hasErrors = [...validationErrors.entries()].some(([_, errors]) => { return Object.values(errors).some(errorSet => - errorSet.some(error => error.source !== 'required') + errorSet.some(error => error.type !== ErrorType.Required) ); }); @@ -1390,7 +1378,7 @@ export const useValidationState = ({ // Create a copy for data modifications const newData = [...data]; // Use Maps for better performance with large datasets - const batchErrors = new Map>(); + const batchErrors = new Map>(); const batchStatuses = new Map(); console.log(`Validating ${data.length} rows`); @@ -1423,7 +1411,7 @@ export const useValidationState = ({ batchPromises.push( new Promise(resolve => { const row = data[rowIndex]; - const fieldErrors: Record = {}; + const fieldErrors: Record = {}; let hasErrors = false; // Set default values for tax_cat and ship_restrictions if not already set @@ -1479,7 +1467,8 @@ export const useValidationState = ({ fieldErrors[key] = [{ message: field.validations?.find(v => v.rule === 'required')?.errorMessage || 'This field is required', level: 'error', - source: 'required' + source: ErrorSources.Row, + type: ErrorType.Required }]; hasErrors = true; } @@ -1490,7 +1479,8 @@ export const useValidationState = ({ fieldErrors['supplier'] = [{ message: 'Supplier is required', level: 'error', - source: 'required' + source: ErrorSources.Row, + type: ErrorType.Required }]; hasErrors = true; } @@ -1499,7 +1489,8 @@ export const useValidationState = ({ fieldErrors['company'] = [{ message: 'Company is required', level: 'error', - source: 'required' + source: ErrorSources.Row, + type: ErrorType.Required }]; hasErrors = true; } diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts index b0847d4..8a410db 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts @@ -1,16 +1,17 @@ -import { InfoWithSource, ErrorLevel } from "../../../types" +import { InfoWithSource, ErrorLevel, ErrorSources, ErrorType as ValidationErrorType } from "../../../types" // Define our own Error type that's compatible with the original export interface ErrorType { message: string; level: ErrorLevel; - source?: string; + source?: ErrorSources; + type: ValidationErrorType; } // Export a namespace to make it accessible at runtime export const ErrorTypes = { - createError: (message: string, level: ErrorLevel = 'error', source: string = 'row'): ErrorType => { - return { message, level, source }; + createError: (message: string, level: ErrorLevel = 'error', source: ErrorSources = ErrorSources.Row, type: ValidationErrorType = ValidationErrorType.Custom): ErrorType => { + return { message, level, source, type }; } }; diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts index 6e3bb0e..382798e 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts @@ -1,7 +1,7 @@ import type { Data, Fields, Info, RowHook, TableHook } from "../../../types" import type { Meta, Error, Errors } from "../types" import { v4 } from "uuid" -import { ErrorSources } from "../../../types" +import { ErrorSources, ErrorType } from "../../../types" type DataWithMeta = Data & Meta & { @@ -18,10 +18,10 @@ export const addErrorsAndRunHooks = async ( ): Promise[]> => { const errors: Errors = {} - const addError = (source: ErrorSources, rowIndex: number, fieldKey: string, error: Info) => { + const addError = (source: ErrorSources, rowIndex: number, fieldKey: string, error: Info, type: ErrorType = ErrorType.Custom) => { errors[rowIndex] = { ...errors[rowIndex], - [fieldKey]: { ...error, source }, + [fieldKey]: { ...error, source, type }, } } @@ -89,7 +89,7 @@ export const addErrorsAndRunHooks = async ( addError(ErrorSources.Table, index, fieldKey, { level: validation.level || "error", message: validation.errorMessage || "Field must be unique", - }) + }, ErrorType.Unique) } }) break @@ -103,7 +103,7 @@ export const addErrorsAndRunHooks = async ( addError(ErrorSources.Row, realIndex, fieldKey, { level: validation.level || "error", message: validation.errorMessage || "Field is required", - }) + }, ErrorType.Required) } }) break @@ -120,7 +120,7 @@ export const addErrorsAndRunHooks = async ( level: validation.level || "error", message: validation.errorMessage || `Field did not match the regex /${validation.value}/${validation.flags} `, - }) + }, ErrorType.Regex) } }) break diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts index 2740741..9fe6592 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts @@ -1,5 +1,5 @@ -import { Field, Data, ErrorSources } from '../../../types' -import { ErrorType } from '../types/index' +import { Field, Data, ErrorSources, ErrorType as ValidationErrorType } from '../../../types' +import { ErrorType } from '../types' /** * Formats a price value to a consistent format @@ -80,17 +80,20 @@ export const validateRegex = (value: any, regex: string, flags?: string): boolea * @param message Error message * @param level Error level * @param source Error source + * @param type Error type * @returns Error object */ export const createError = ( message: string, level: 'info' | 'warning' | 'error' = 'error', - source: ErrorSources = ErrorSources.Row + source: ErrorSources = ErrorSources.Row, + type: ValidationErrorType = ValidationErrorType.Custom ): ErrorType => { return { message, level, - source + source, + type } as ErrorType } @@ -136,7 +139,8 @@ export const validateSpecialFields = (row: Data): Record(row: Data): Record = {