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

@@ -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(({
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20">
<ValidationIcon error={{
message: errorMessages,
level: 'error'
level: 'error',
type: ErrorType.Custom
}} />
</div>
)}
@@ -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 = ({
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20">
<ValidationIcon error={{
message: errorMessages,
level: 'error'
level: 'error',
type: ErrorType.Custom
}} />
</div>
)}

View File

@@ -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 = <T extends string>({
...(rowToUpdate.__errors || {}),
[fieldKey]: {
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 { 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 = <T extends string>(
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 = <T extends string>(
return [key, {
message: errorArray[0].message,
level: errorArray[0].level,
source: ErrorSources.Table
source: ErrorSources.Table,
type: ErrorType.Custom
} as InfoWithSource]
})
) : null

View File

@@ -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 = <T extends string>(
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 = <T extends string>(
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 = <T extends string>(
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 = <T extends string>(
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 = <T extends string>(
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 = <T extends string>(
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 = <T extends string>(
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 = <T extends string>(
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
}

View File

@@ -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<T extends string> {
@@ -26,7 +34,7 @@ export interface Props<T extends string> {
// Extended Data type with meta information
export type RowData<T extends string> = Data<T> & {
__index?: string;
__errors?: Record<string, ErrorType[] | ErrorType>;
__errors?: Record<string, ValidationError[] | ValidationError>;
__template?: string;
__original?: Record<string, any>;
__corrected?: Record<string, any>;
@@ -55,6 +63,8 @@ export interface ValidationResult {
error?: boolean;
message?: string;
data?: Record<string, any>;
type?: ErrorType;
source?: ErrorSources;
}
// Filter state interface
@@ -178,7 +188,7 @@ export const useValidationState = <T extends string>({
// Validation state
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 [, setValidatingCells] = useState<Set<string>>(new Set())
@@ -212,7 +222,7 @@ export const useValidationState = <T extends string>({
// Add batch update state
const pendingUpdatesRef = useRef<{
errors: Map<number, Record<string, ErrorType[]>>,
errors: Map<number, Record<string, ValidationError[]>>,
statuses: Map<number, 'pending' | 'validating' | 'validated' | 'error'>,
data: Array<RowData<T>>
}>({
@@ -357,7 +367,7 @@ export const useValidationState = <T extends string>({
// Queue updates instead of immediate setState calls
const queueUpdate = useCallback((rowIndex: number, updates: {
errors?: Record<string, ErrorType[]>,
errors?: Record<string, ValidationError[]>,
status?: 'pending' | 'validating' | 'validated' | 'error',
data?: RowData<T>
}) => {
@@ -384,7 +394,7 @@ export const useValidationState = <T extends string>({
const itemNumberMap = new Map<string, number[]>();
// 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
data.forEach((row, index) => {
@@ -403,8 +413,9 @@ export const useValidationState = <T extends string>({
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 = <T extends string>({
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 = <T extends string>({
}, [])
// Helper function to validate a field value
const validateField = useCallback((value: any, field: Field<T>): ErrorType[] => {
const errors: ErrorType[] = [];
const validateField = useCallback((value: any, field: Field<T>): 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 = <T extends string>({
// Get the row data
const row = data[rowIndex];
const fieldErrors: Record<string, ErrorType[]> = {};
const fieldErrors: Record<string, ValidationError[]> = {};
let hasErrors = false;
// Get current errors for comparison
@@ -823,7 +809,8 @@ export const useValidationState = <T extends string>({
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 = <T extends string>({
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 = <T extends string>({
// Create a copy of data and process all rows at once to minimize state updates
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'>();
// 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
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 = <T extends string>({
// Create a copy for data modifications
const newData = [...data];
// 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'>();
console.log(`Validating ${data.length} rows`);
@@ -1423,7 +1411,7 @@ export const useValidationState = <T extends string>({
batchPromises.push(
new Promise<void>(resolve => {
const row = data[rowIndex];
const fieldErrors: Record<string, ErrorType[]> = {};
const fieldErrors: Record<string, ValidationError[]> = {};
let hasErrors = false;
// 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] = [{
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 = <T extends string>({
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 = <T extends string>({
fieldErrors['company'] = [{
message: 'Company is required',
level: 'error',
source: 'required'
source: ErrorSources.Row,
type: ErrorType.Required
}];
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
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 };
}
};

View File

@@ -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<T extends string> = Data<T> & Meta & {
@@ -18,10 +18,10 @@ export const addErrorsAndRunHooks = async <T extends string>(
): Promise<DataWithMeta<T>[]> => {
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 <T extends string>(
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 <T extends string>(
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 <T extends string>(
level: validation.level || "error",
message:
validation.errorMessage || `Field did not match the regex /${validation.value}/${validation.flags} `,
})
}, ErrorType.Regex)
}
})
break

View File

@@ -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 = <T extends string>(row: Data<T>): Record<st
errors['supplier'] = [{
message: 'Supplier is required',
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'] = [{
message: 'Company is required',
level: 'error',
source: ErrorSources.Row
source: ErrorSources.Row,
type: ValidationErrorType.Required
}]
}

View File

@@ -163,8 +163,27 @@ export type Info = {
}
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
*/
export type InfoWithSource = Info & {
source: ErrorSources
source: ErrorSources;
type: ErrorType;
}
export type Result<T extends string> = {