Add standardized error handling with new enums and interfaces for validation errors

This commit is contained in:
2025-03-15 22:11:36 -04:00
parent cb46970808
commit 153bbecc44
10 changed files with 398 additions and 132 deletions

View File

@@ -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<Record<number, NodeJS.Timeout>>({});
```
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<string, InfoWithSource> = {}
// 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

View File

@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { Field } from '../../../types' import { Field, ErrorType } from '../../../types'
import { Loader2, AlertCircle, ArrowDown, Check, X } from 'lucide-react' import { Loader2, AlertCircle, ArrowDown, Check, X } from 'lucide-react'
import { import {
Tooltip, Tooltip,
@@ -40,6 +40,7 @@ type ErrorObject = {
message: string; message: string;
level: string; level: string;
source?: string; source?: string;
type?: ErrorType;
} }
// Helper function to check if a value is empty - utility function shared by all components // 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 // Use the shared isEmpty function instead of defining a local one
const valueIsEmpty = value === undefined || const valueIsEmpty = isEmpty(value);
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
// If not empty, filter out required errors // If not empty, filter out required errors
// Create a new array only if we need to filter (avoid unnecessary allocations) // 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) { if (valueIsEmpty) {
// For empty values, check if there are required errors // For empty values, check if there are required errors
hasRequiredError = errors.some(error => hasRequiredError = errors.some(error =>
error.message?.toLowerCase().includes('required') error.type === ErrorType.Required
); );
filteredErrors = errors; filteredErrors = errors;
} else { } else {
// For non-empty values, filter out required errors // For non-empty values, filter out required errors
filteredErrors = errors.filter(error => filteredErrors = errors.filter(error =>
!error.message?.toLowerCase().includes('required') error.type !== ErrorType.Required
); );
} }
@@ -351,7 +348,8 @@ const ItemNumberCell = React.memo(({
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20"> <div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20">
<ValidationIcon error={{ <ValidationIcon error={{
message: errorMessages, message: errorMessages,
level: 'error' level: 'error',
type: ErrorType.Custom
}} /> }} />
</div> </div>
)} )}
@@ -474,7 +472,7 @@ const ValidationCell = ({
// Memoize filtered errors to avoid recalculation on every render // Memoize filtered errors to avoid recalculation on every render
const filteredErrors = React.useMemo(() => { const filteredErrors = React.useMemo(() => {
return !isEmpty(value) return !isEmpty(value)
? errors.filter(error => !error.message?.toLowerCase().includes('required')) ? errors.filter(error => error.type !== ErrorType.Required)
: errors; : errors;
}, [value, errors]); }, [value, errors]);
@@ -485,7 +483,7 @@ const ValidationCell = ({
// Determine if the field is required but empty // Determine if the field is required but empty
const isRequiredButEmpty = isEmpty(value) && 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) // Only show error icons for non-empty fields with actual errors (not just required errors)
const shouldShowErrorIcon = hasError && !isEmpty(value); const shouldShowErrorIcon = hasError && !isEmpty(value);
@@ -555,7 +553,8 @@ const ValidationCell = ({
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20"> <div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20">
<ValidationIcon error={{ <ValidationIcon error={{
message: errorMessages, message: errorMessages,
level: 'error' level: 'error',
type: ErrorType.Custom
}} /> }} />
</div> </div>
)} )}

View File

@@ -10,7 +10,7 @@ import SearchableTemplateSelect from './SearchableTemplateSelect'
import { useAiValidation } from '../hooks/useAiValidation' import { useAiValidation } from '../hooks/useAiValidation'
import { AiValidationDialogs } from './AiValidationDialogs' import { AiValidationDialogs } from './AiValidationDialogs'
import config from '@/config' import config from '@/config'
import { Fields } from '../../../types' import { Fields, ErrorSources, ErrorType } from '../../../types'
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog' import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
import { TemplateForm } from '@/components/templates/TemplateForm' import { TemplateForm } from '@/components/templates/TemplateForm'
import axios from 'axios' import axios from 'axios'
@@ -229,7 +229,9 @@ const ValidationContainer = <T extends string>({
...(rowToUpdate.__errors || {}), ...(rowToUpdate.__errors || {}),
[fieldKey]: { [fieldKey]: {
level: 'error', level: 'error',
message: `UPC already exists (${errorData.existingItemNumber})` message: `UPC already exists (${errorData.existingItemNumber})`,
source: ErrorSources.Upc,
type: ErrorType.Unique
} }
} }
}; };

View File

@@ -1,7 +1,7 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getApiUrl, RowData } from './useValidationState'; import { getApiUrl, RowData } from './useValidationState';
import { Fields, InfoWithSource, ErrorSources } from '../../../types'; import { Fields, InfoWithSource, ErrorSources, ErrorType } from '../../../types';
import { Meta } from '../types'; import { Meta } from '../types';
import { addErrorsAndRunHooks } from '../utils/dataMutations'; import { addErrorsAndRunHooks } from '../utils/dataMutations';
import * as Diff from 'diff'; import * as Diff from 'diff';
@@ -99,7 +99,8 @@ export const useAiValidation = <T extends string>(
return [key, { return [key, {
message: errorArray[0].message, message: errorArray[0].message,
level: errorArray[0].level, level: errorArray[0].level,
source: ErrorSources.Row source: ErrorSources.Row,
type: ErrorType.Custom
} as InfoWithSource] } as InfoWithSource]
}) })
) : null ) : null
@@ -120,7 +121,8 @@ export const useAiValidation = <T extends string>(
return [key, { return [key, {
message: errorArray[0].message, message: errorArray[0].message,
level: errorArray[0].level, level: errorArray[0].level,
source: ErrorSources.Table source: ErrorSources.Table,
type: ErrorType.Custom
} as InfoWithSource] } as InfoWithSource]
}) })
) : null ) : null

View File

@@ -1,18 +1,16 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { Field, Fields, RowHook, TableHook } from '../../../types' import type { Field, Fields, RowHook, TableHook } from '../../../types'
import type { Meta } from '../types' import type { Meta } from '../types'
import { ErrorSources } from '../../../types' import { ErrorSources, ErrorType, ValidationError } from '../../../types'
import { RowData } from './useValidationState' import { RowData } from './useValidationState'
interface ValidationError { // Define InfoWithSource to match the expected structure
message: string // Make sure source is required (not optional)
level: 'info' | 'warning' | 'error'
}
interface InfoWithSource { interface InfoWithSource {
message: string message: string;
level: 'info' | 'warning' | 'error' level: 'info' | 'warning' | 'error';
source: ErrorSources source: ErrorSources;
type: ErrorType;
} }
// Shared utility function for checking empty values - defined once to avoid duplication // Shared utility function for checking empty values - defined once to avoid duplication
@@ -44,7 +42,8 @@ export const useValidation = <T extends string>(
if (isEmpty(value)) { if (isEmpty(value)) {
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',
type: ErrorType.Required
}) })
} }
break break
@@ -60,7 +59,8 @@ export const useValidation = <T extends string>(
if (!regex.test(String(value))) { if (!regex.test(String(value))) {
errors.push({ errors.push({
message: validation.errorMessage, message: validation.errorMessage,
level: validation.level || 'error' level: validation.level || 'error',
type: ErrorType.Regex
}) })
} }
} catch (error) { } catch (error) {
@@ -98,14 +98,16 @@ export const useValidation = <T extends string>(
if (fields.some(field => String(field.key) === 'supplier') && isEmpty(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',
type: ErrorType.Required
}] }]
} }
if (fields.some(field => String(field.key) === 'company') && isEmpty(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',
type: ErrorType.Required
}] }]
} }
@@ -131,7 +133,8 @@ export const useValidation = <T extends string>(
mergedErrors[key] = { mergedErrors[key] = {
message: errors[0].message, message: errors[0].message,
level: errors[0].level, level: errors[0].level,
source: ErrorSources.Row source: ErrorSources.Row,
type: errors[0].type || ErrorType.Custom
} }
} }
}) })
@@ -140,7 +143,12 @@ export const useValidation = <T extends string>(
if (rowHookResult.__errors) { if (rowHookResult.__errors) {
Object.entries(rowHookResult.__errors).forEach(([key, error]) => { Object.entries(rowHookResult.__errors).forEach(([key, error]) => {
if (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 = <T extends string>(
if (error) { if (error) {
formattedErrors[key] = { formattedErrors[key] = {
...error, ...error,
source: ErrorSources.Table source: ErrorSources.Table,
} type: error.type || ErrorType.Custom
} as InfoWithSource
} }
}) })
} }
@@ -246,7 +255,8 @@ export const useValidation = <T extends string>(
rowErrors[String(key)] = { rowErrors[String(key)] = {
message: errorMessage, message: errorMessage,
level, level,
source: ErrorSources.Table source: ErrorSources.Table,
type: ErrorType.Unique
} }
uniqueErrors[rowIndex].__errors = rowErrors uniqueErrors[rowIndex].__errors = rowErrors
@@ -295,12 +305,12 @@ export const useValidation = <T extends string>(
Object.entries(combinedErrors).forEach(([key, error]) => { Object.entries(combinedErrors).forEach(([key, error]) => {
const fieldValue = row[key as keyof typeof row] 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) && if (!isEmpty(fieldValue) &&
error && error &&
typeof error === 'object' && typeof error === 'object' &&
'message' in error && 'type' in error &&
error.message?.toLowerCase().includes('required')) { error.type === ErrorType.Required) {
return return
} }

View File

@@ -1,18 +1,26 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { useRsi } from '../../../hooks/useRsi' import { useRsi } from '../../../hooks/useRsi'
import type { Data, Field } from '../../../types' import type { Data, Field } from '../../../types'
import { ErrorSources, ErrorType, ValidationError } from '../../../types'
import { RowSelectionState } from '@tanstack/react-table' import { RowSelectionState } from '@tanstack/react-table'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import config from "@/config"; import config from "@/config";
// Define ErrorType directly in file to avoid import issues // Helper function to check if a value is empty
type ErrorType = { const isEmpty = (val: any): boolean =>
message: string; val === undefined ||
level: string; val === null ||
source?: string; 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 // Define the Props interface for ValidationStepNew
export interface Props<T extends string> { export interface Props<T extends string> {
@@ -26,7 +34,7 @@ export interface Props<T extends string> {
// Extended Data type with meta information // Extended Data type with meta information
export type RowData<T extends string> = Data<T> & { export type RowData<T extends string> = Data<T> & {
__index?: string; __index?: string;
__errors?: Record<string, ErrorType[] | ErrorType>; __errors?: Record<string, ValidationError[] | ValidationError>;
__template?: string; __template?: string;
__original?: Record<string, any>; __original?: Record<string, any>;
__corrected?: Record<string, any>; __corrected?: Record<string, any>;
@@ -55,6 +63,8 @@ export interface ValidationResult {
error?: boolean; error?: boolean;
message?: string; message?: string;
data?: Record<string, any>; data?: Record<string, any>;
type?: ErrorType;
source?: ErrorSources;
} }
// Filter state interface // Filter state interface
@@ -178,7 +188,7 @@ export const useValidationState = <T extends string>({
// Validation state // Validation state
const [isValidating] = useState(false) const [isValidating] = useState(false)
const [validationErrors, setValidationErrors] = useState<Map<number, Record<string, ErrorType[]>>>(new Map()) const [validationErrors, setValidationErrors] = useState<Map<number, Record<string, ValidationError[]>>>(new Map())
const [rowValidationStatus, setRowValidationStatus] = useState<Map<number, 'pending' | 'validating' | 'validated' | 'error'>>(new Map()) const [rowValidationStatus, setRowValidationStatus] = useState<Map<number, 'pending' | 'validating' | 'validated' | 'error'>>(new Map())
const [, setValidatingCells] = useState<Set<string>>(new Set()) const [, setValidatingCells] = useState<Set<string>>(new Set())
@@ -212,7 +222,7 @@ export const useValidationState = <T extends string>({
// Add batch update state // Add batch update state
const pendingUpdatesRef = useRef<{ const pendingUpdatesRef = useRef<{
errors: Map<number, Record<string, ErrorType[]>>, errors: Map<number, Record<string, ValidationError[]>>,
statuses: Map<number, 'pending' | 'validating' | 'validated' | 'error'>, statuses: Map<number, 'pending' | 'validating' | 'validated' | 'error'>,
data: Array<RowData<T>> data: Array<RowData<T>>
}>({ }>({
@@ -357,7 +367,7 @@ export const useValidationState = <T extends string>({
// Queue updates instead of immediate setState calls // Queue updates instead of immediate setState calls
const queueUpdate = useCallback((rowIndex: number, updates: { const queueUpdate = useCallback((rowIndex: number, updates: {
errors?: Record<string, ErrorType[]>, errors?: Record<string, ValidationError[]>,
status?: 'pending' | 'validating' | 'validated' | 'error', status?: 'pending' | 'validating' | 'validated' | 'error',
data?: RowData<T> data?: RowData<T>
}) => { }) => {
@@ -384,7 +394,7 @@ export const useValidationState = <T extends string>({
const itemNumberMap = new Map<string, number[]>(); const itemNumberMap = new Map<string, number[]>();
// Initialize batch updates // Initialize batch updates
const errors = new Map<number, Record<string, ErrorType[]>>(); const errors = new Map<number, Record<string, ValidationError[]>>();
// Single pass through data to identify all item numbers // Single pass through data to identify all item numbers
data.forEach((row, index) => { data.forEach((row, index) => {
@@ -403,8 +413,9 @@ export const useValidationState = <T extends string>({
if (indices.length > 1) { if (indices.length > 1) {
const errorObj = { const errorObj = {
message: `Duplicate item number: ${itemNumber}`, message: `Duplicate item number: ${itemNumber}`,
level: 'error', level: 'error' as 'error',
source: 'validation' source: ErrorSources.Table,
type: ErrorType.Unique
}; };
// Add error to each row with this item number // Add error to each row with this item number
@@ -470,10 +481,12 @@ export const useValidationState = <T extends string>({
const errorData = await response.json(); const errorData = await response.json();
return { return {
error: true, error: true,
message: 'UPC already exists', message: `UPC already exists (${errorData.existingItemNumber})`,
data: { data: {
itemNumber: errorData.existingItemNumber || '', itemNumber: errorData.existingItemNumber || '',
} },
type: ErrorType.Unique,
source: ErrorSources.Api
}; };
} else if (response.status === 429) { } else if (response.status === 429) {
return { return {
@@ -691,65 +704,38 @@ export const useValidationState = <T extends string>({
}, []) }, [])
// Helper function to validate a field value // Helper function to validate a field value
const validateField = useCallback((value: any, field: Field<T>): ErrorType[] => { const validateField = useCallback((value: any, field: Field<T>): ValidationError[] => {
const errors: ErrorType[] = []; const errors: ValidationError[] = [];
// Required field validation - improved to better handle various value types // Required field validation - improved to better handle various value types
if (field.validations?.some(v => v.rule === 'required')) { if (field.validations?.some(v => v.rule === 'required')) {
// Handle different value types more carefully if (value === undefined || value === null || value === '' ||
const isEmpty = (Array.isArray(value) && value.length === 0) ||
value === undefined || (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)) {
value === null ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0);
if (isEmpty) {
errors.push({ errors.push({
message: 'Required field', message: field.validations.find(v => v.rule === 'required')?.errorMessage || `${field.label} is required`,
level: 'error', 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 // Regex validation
if (field.validations) { const regexValidation = field.validations?.find(v => v.rule === 'regex');
for (const validation of field.validations) { if (regexValidation && value !== undefined && value !== null && value !== '') {
// Skip required validation as we've already handled it try {
if (validation.rule === 'required') continue; const regex = new RegExp(regexValidation.value, regexValidation.flags);
if (!regex.test(String(value))) {
if (validation.rule === 'regex' && typeof value === 'string') { errors.push({
// Implement regex validation message: regexValidation.errorMessage,
const regex = new RegExp(validation.value!); level: regexValidation.level || 'error',
if (!regex.test(value)) { source: ErrorSources.Row,
errors.push({ type: ErrorType.Regex
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'
});
}
} }
// Add other validation types as needed } catch (error) {
console.error('Invalid regex in validation:', error);
} }
} }
@@ -770,7 +756,7 @@ export const useValidationState = <T extends string>({
// Get the row data // Get the row data
const row = data[rowIndex]; const row = data[rowIndex];
const fieldErrors: Record<string, ErrorType[]> = {}; const fieldErrors: Record<string, ValidationError[]> = {};
let hasErrors = false; let hasErrors = false;
// Get current errors for comparison // Get current errors for comparison
@@ -823,7 +809,8 @@ export const useValidationState = <T extends string>({
fieldErrors['supplier'] = [{ fieldErrors['supplier'] = [{
message: 'Supplier is required', message: 'Supplier is required',
level: 'error', level: 'error',
source: 'required' source: ErrorSources.Row,
type: ErrorType.Required
}]; }];
hasErrors = true; hasErrors = true;
} }
@@ -831,7 +818,8 @@ export const useValidationState = <T extends string>({
fieldErrors['company'] = [{ fieldErrors['company'] = [{
message: 'Company is required', message: 'Company is required',
level: 'error', level: 'error',
source: 'required' source: ErrorSources.Row,
type: ErrorType.Required
}]; }];
hasErrors = true; hasErrors = true;
} }
@@ -1095,7 +1083,7 @@ export const useValidationState = <T extends string>({
// Create a copy of data and process all rows at once to minimize state updates // Create a copy of data and process all rows at once to minimize state updates
const newData = [...data]; const newData = [...data];
const batchErrors = new Map<number, Record<string, ErrorType[]>>(); const batchErrors = new Map<number, Record<string, ValidationError[]>>();
const batchStatuses = new Map<number, 'pending' | 'validating' | 'validated' | 'error'>(); const batchStatuses = new Map<number, 'pending' | 'validating' | 'validated' | 'error'>();
// Extract template fields once outside the loop // Extract template fields once outside the loop
@@ -1337,7 +1325,7 @@ export const useValidationState = <T extends string>({
// When proceeding to the next screen, check for unvalidated rows first // When proceeding to the next screen, check for unvalidated rows first
const hasErrors = [...validationErrors.entries()].some(([_, errors]) => { const hasErrors = [...validationErrors.entries()].some(([_, errors]) => {
return Object.values(errors).some(errorSet => 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 = <T extends string>({
// Create a copy for data modifications // Create a copy for data modifications
const newData = [...data]; const newData = [...data];
// Use Maps for better performance with large datasets // Use Maps for better performance with large datasets
const batchErrors = new Map<number, Record<string, ErrorType[]>>(); const batchErrors = new Map<number, Record<string, ValidationError[]>>();
const batchStatuses = new Map<number, 'pending' | 'validating' | 'validated' | 'error'>(); const batchStatuses = new Map<number, 'pending' | 'validating' | 'validated' | 'error'>();
console.log(`Validating ${data.length} rows`); console.log(`Validating ${data.length} rows`);
@@ -1423,7 +1411,7 @@ export const useValidationState = <T extends string>({
batchPromises.push( batchPromises.push(
new Promise<void>(resolve => { new Promise<void>(resolve => {
const row = data[rowIndex]; const row = data[rowIndex];
const fieldErrors: Record<string, ErrorType[]> = {}; const fieldErrors: Record<string, ValidationError[]> = {};
let hasErrors = false; let hasErrors = false;
// Set default values for tax_cat and ship_restrictions if not already set // Set default values for tax_cat and ship_restrictions if not already set
@@ -1479,7 +1467,8 @@ export const useValidationState = <T extends string>({
fieldErrors[key] = [{ fieldErrors[key] = [{
message: field.validations?.find(v => v.rule === 'required')?.errorMessage || 'This field is required', message: field.validations?.find(v => v.rule === 'required')?.errorMessage || 'This field is required',
level: 'error', level: 'error',
source: 'required' source: ErrorSources.Row,
type: ErrorType.Required
}]; }];
hasErrors = true; hasErrors = true;
} }
@@ -1490,7 +1479,8 @@ export const useValidationState = <T extends string>({
fieldErrors['supplier'] = [{ fieldErrors['supplier'] = [{
message: 'Supplier is required', message: 'Supplier is required',
level: 'error', level: 'error',
source: 'required' source: ErrorSources.Row,
type: ErrorType.Required
}]; }];
hasErrors = true; hasErrors = true;
} }
@@ -1499,7 +1489,8 @@ export const useValidationState = <T extends string>({
fieldErrors['company'] = [{ fieldErrors['company'] = [{
message: 'Company is required', message: 'Company is required',
level: 'error', level: 'error',
source: 'required' source: ErrorSources.Row,
type: ErrorType.Required
}]; }];
hasErrors = true; hasErrors = true;
} }

View File

@@ -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 // Define our own Error type that's compatible with the original
export interface ErrorType { export interface ErrorType {
message: string; message: string;
level: ErrorLevel; level: ErrorLevel;
source?: string; source?: ErrorSources;
type: ValidationErrorType;
} }
// Export a namespace to make it accessible at runtime // Export a namespace to make it accessible at runtime
export const ErrorTypes = { export const ErrorTypes = {
createError: (message: string, level: ErrorLevel = 'error', source: string = 'row'): ErrorType => { createError: (message: string, level: ErrorLevel = 'error', source: ErrorSources = ErrorSources.Row, type: ValidationErrorType = ValidationErrorType.Custom): ErrorType => {
return { message, level, source }; return { message, level, source, type };
} }
}; };

View File

@@ -1,7 +1,7 @@
import type { Data, Fields, Info, RowHook, TableHook } from "../../../types" import type { Data, Fields, Info, RowHook, TableHook } from "../../../types"
import type { Meta, Error, Errors } from "../types" import type { Meta, Error, Errors } from "../types"
import { v4 } from "uuid" import { v4 } from "uuid"
import { ErrorSources } from "../../../types" import { ErrorSources, ErrorType } from "../../../types"
type DataWithMeta<T extends string> = Data<T> & Meta & { type DataWithMeta<T extends string> = Data<T> & Meta & {
@@ -18,10 +18,10 @@ export const addErrorsAndRunHooks = async <T extends string>(
): Promise<DataWithMeta<T>[]> => { ): Promise<DataWithMeta<T>[]> => {
const errors: Errors = {} 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] = {
...errors[rowIndex], ...errors[rowIndex],
[fieldKey]: { ...error, source }, [fieldKey]: { ...error, source, type },
} }
} }
@@ -89,7 +89,7 @@ export const addErrorsAndRunHooks = async <T extends string>(
addError(ErrorSources.Table, index, fieldKey, { addError(ErrorSources.Table, index, fieldKey, {
level: validation.level || "error", level: validation.level || "error",
message: validation.errorMessage || "Field must be unique", message: validation.errorMessage || "Field must be unique",
}) }, ErrorType.Unique)
} }
}) })
break break
@@ -103,7 +103,7 @@ export const addErrorsAndRunHooks = async <T extends string>(
addError(ErrorSources.Row, realIndex, fieldKey, { addError(ErrorSources.Row, realIndex, fieldKey, {
level: validation.level || "error", level: validation.level || "error",
message: validation.errorMessage || "Field is required", message: validation.errorMessage || "Field is required",
}) }, ErrorType.Required)
} }
}) })
break break
@@ -120,7 +120,7 @@ export const addErrorsAndRunHooks = async <T extends string>(
level: validation.level || "error", level: validation.level || "error",
message: message:
validation.errorMessage || `Field did not match the regex /${validation.value}/${validation.flags} `, validation.errorMessage || `Field did not match the regex /${validation.value}/${validation.flags} `,
}) }, ErrorType.Regex)
} }
}) })
break break

View File

@@ -1,5 +1,5 @@
import { Field, Data, ErrorSources } from '../../../types' import { Field, Data, ErrorSources, ErrorType as ValidationErrorType } from '../../../types'
import { ErrorType } from '../types/index' import { ErrorType } from '../types'
/** /**
* Formats a price value to a consistent format * 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 message Error message
* @param level Error level * @param level Error level
* @param source Error source * @param source Error source
* @param type Error type
* @returns Error object * @returns Error object
*/ */
export const createError = ( export const createError = (
message: string, message: string,
level: 'info' | 'warning' | 'error' = 'error', level: 'info' | 'warning' | 'error' = 'error',
source: ErrorSources = ErrorSources.Row source: ErrorSources = ErrorSources.Row,
type: ValidationErrorType = ValidationErrorType.Custom
): ErrorType => { ): ErrorType => {
return { return {
message, message,
level, level,
source source,
type
} as ErrorType } as ErrorType
} }
@@ -136,7 +139,8 @@ export const validateSpecialFields = <T extends string>(row: Data<T>): Record<st
errors['supplier'] = [{ errors['supplier'] = [{
message: 'Supplier is required', message: 'Supplier is required',
level: 'error', level: 'error',
source: ErrorSources.Row source: ErrorSources.Row,
type: ValidationErrorType.Required
}] }]
} }
@@ -145,7 +149,8 @@ export const validateSpecialFields = <T extends string>(row: Data<T>): Record<st
errors['company'] = [{ errors['company'] = [{
message: 'Company is required', message: 'Company is required',
level: 'error', level: 'error',
source: ErrorSources.Row source: ErrorSources.Row,
type: ValidationErrorType.Required
}] }]
} }

View File

@@ -163,8 +163,27 @@ export type Info = {
} }
export enum ErrorSources { export enum ErrorSources {
Table = "table", Row = 'row',
Row = "row", Table = 'table',
Api = 'api',
Upc = 'upc'
}
// Add a standardized error type enum
export enum ErrorType {
Required = 'required',
Regex = 'regex',
Unique = 'unique',
Custom = 'custom',
Api = 'api'
}
// Add a standardized error interface
export interface ValidationError {
message: string;
level: 'info' | 'warning' | 'error';
source?: ErrorSources;
type: ErrorType;
} }
/* /*
@@ -174,7 +193,8 @@ export enum ErrorSources {
it is used to determine if row.__errors should be updated or not depending on different validations it is used to determine if row.__errors should be updated or not depending on different validations
*/ */
export type InfoWithSource = Info & { export type InfoWithSource = Info & {
source: ErrorSources source: ErrorSources;
type: ErrorType;
} }
export type Result<T extends string> = { export type Result<T extends string> = {