Refactor validation hooks into smaller files
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getApiUrl, RowData } from './useValidationState';
|
||||
import { getApiUrl, RowData } from './validationTypes';
|
||||
import { Fields } from '../../../types';
|
||||
import { Meta } from '../types';
|
||||
import { addErrorsAndRunHooks } from '../utils/dataMutations';
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { Field, Fields, RowHook, TableHook } from '../../../types';
|
||||
import type { Meta } from '../types';
|
||||
import { ErrorSources, ErrorType, ValidationError } from '../../../types';
|
||||
import { RowData, InfoWithSource, isEmpty } from './validationTypes';
|
||||
|
||||
// Create a cache for validation results to avoid repeated validation of the same data
|
||||
const validationResultCache = new Map();
|
||||
|
||||
// Add a function to clear cache for a specific field value
|
||||
export const clearValidationCacheForField = (fieldKey: string) => {
|
||||
// Look for entries that match this field key
|
||||
validationResultCache.forEach((_, key) => {
|
||||
if (key.startsWith(`${fieldKey}-`)) {
|
||||
validationResultCache.delete(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add a special function to clear all uniqueness validation caches
|
||||
export const clearAllUniquenessCaches = () => {
|
||||
// Clear cache for common unique fields
|
||||
['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'].forEach(fieldKey => {
|
||||
clearValidationCacheForField(fieldKey);
|
||||
});
|
||||
|
||||
// Also clear any cache entries that might involve uniqueness validation
|
||||
validationResultCache.forEach((_, key) => {
|
||||
if (key.includes('unique')) {
|
||||
validationResultCache.delete(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useFieldValidation = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
rowHook?: RowHook<T>,
|
||||
tableHook?: TableHook<T>
|
||||
) => {
|
||||
// Validate a single field
|
||||
const validateField = useCallback((
|
||||
value: any,
|
||||
field: Field<T>
|
||||
): ValidationError[] => {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!field.validations) return errors;
|
||||
|
||||
// Create a cache key using field key, value, and validation rules
|
||||
const cacheKey = `${field.key}-${String(value)}-${JSON.stringify(field.validations)}`;
|
||||
|
||||
// Check cache first to avoid redundant validation
|
||||
if (validationResultCache.has(cacheKey)) {
|
||||
return validationResultCache.get(cacheKey) || [];
|
||||
}
|
||||
|
||||
field.validations.forEach(validation => {
|
||||
switch (validation.rule) {
|
||||
case 'required':
|
||||
// Use the shared isEmpty function
|
||||
if (isEmpty(value)) {
|
||||
errors.push({
|
||||
message: validation.errorMessage || 'This field is required',
|
||||
level: validation.level || 'error',
|
||||
type: ErrorType.Required
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'unique':
|
||||
// Unique validation happens at table level, not here
|
||||
break;
|
||||
|
||||
case 'regex':
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
try {
|
||||
const regex = new RegExp(validation.value, validation.flags);
|
||||
if (!regex.test(String(value))) {
|
||||
errors.push({
|
||||
message: validation.errorMessage,
|
||||
level: validation.level || 'error',
|
||||
type: ErrorType.Regex
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invalid regex in validation:', error);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Store results in cache to speed up future validations
|
||||
validationResultCache.set(cacheKey, errors);
|
||||
|
||||
return errors;
|
||||
}, []);
|
||||
|
||||
// Validate a single row
|
||||
const validateRow = useCallback(async (
|
||||
row: RowData<T>,
|
||||
rowIndex: number,
|
||||
allRows: RowData<T>[]
|
||||
): Promise<Meta> => {
|
||||
// Run field-level validations
|
||||
const fieldErrors: Record<string, ValidationError[]> = {};
|
||||
|
||||
fields.forEach(field => {
|
||||
const value = row[String(field.key) as keyof typeof row];
|
||||
const errors = validateField(value, field as Field<T>);
|
||||
|
||||
if (errors.length > 0) {
|
||||
fieldErrors[String(field.key)] = errors;
|
||||
}
|
||||
});
|
||||
|
||||
// Special validation for supplier and company fields - only apply if the field exists in fields
|
||||
if (fields.some(field => String(field.key) === 'supplier') && isEmpty(row.supplier)) {
|
||||
fieldErrors['supplier'] = [{
|
||||
message: 'Supplier is required',
|
||||
level: 'error',
|
||||
type: ErrorType.Required
|
||||
}];
|
||||
}
|
||||
|
||||
if (fields.some(field => String(field.key) === 'company') && isEmpty(row.company)) {
|
||||
fieldErrors['company'] = [{
|
||||
message: 'Company is required',
|
||||
level: 'error',
|
||||
type: ErrorType.Required
|
||||
}];
|
||||
}
|
||||
|
||||
// Run row hook if provided
|
||||
let rowHookResult: Meta = {
|
||||
__index: row.__index || String(rowIndex)
|
||||
};
|
||||
if (rowHook) {
|
||||
try {
|
||||
// Call the row hook and extract only the __index property
|
||||
const result = await rowHook(row, rowIndex, allRows);
|
||||
rowHookResult.__index = result.__index || rowHookResult.__index;
|
||||
} catch (error) {
|
||||
console.error('Error in row hook:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// We no longer need to merge errors since we're not storing them in the row data
|
||||
// The calling code should handle storing errors in the validationErrors Map
|
||||
|
||||
return {
|
||||
__index: row.__index || String(rowIndex)
|
||||
};
|
||||
}, [fields, validateField, rowHook]);
|
||||
|
||||
return {
|
||||
validateField,
|
||||
validateRow,
|
||||
clearValidationCacheForField,
|
||||
clearAllUniquenessCaches
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { FilterState, RowData } from './validationTypes';
|
||||
import type { Fields } from '../../../types';
|
||||
import { ValidationError } from '../../../types';
|
||||
|
||||
export const useFilterManagement = <T extends string>(
|
||||
data: RowData<T>[],
|
||||
fields: Fields<T>,
|
||||
validationErrors: Map<number, Record<string, ValidationError[]>>
|
||||
) => {
|
||||
// Filter state
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
searchText: "",
|
||||
showErrorsOnly: false,
|
||||
filterField: null,
|
||||
filterValue: null,
|
||||
});
|
||||
|
||||
// Filter data based on current filter state
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((row, index) => {
|
||||
// Filter by search text
|
||||
if (filters.searchText) {
|
||||
const searchLower = filters.searchText.toLowerCase();
|
||||
const matchesSearch = fields.some((field) => {
|
||||
const value = row[field.key as keyof typeof row];
|
||||
if (value === undefined || value === null) return false;
|
||||
return String(value).toLowerCase().includes(searchLower);
|
||||
});
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// Filter by errors
|
||||
if (filters.showErrorsOnly) {
|
||||
const hasErrors =
|
||||
validationErrors.has(index) &&
|
||||
Object.keys(validationErrors.get(index) || {}).length > 0;
|
||||
if (!hasErrors) return false;
|
||||
}
|
||||
|
||||
// Filter by field value
|
||||
if (filters.filterField && filters.filterValue) {
|
||||
const fieldValue = row[filters.filterField as keyof typeof row];
|
||||
if (fieldValue === undefined) return false;
|
||||
|
||||
const valueStr = String(fieldValue).toLowerCase();
|
||||
const filterStr = filters.filterValue.toLowerCase();
|
||||
|
||||
if (!valueStr.includes(filterStr)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [data, fields, filters, validationErrors]);
|
||||
|
||||
// Get filter fields
|
||||
const filterFields = useMemo(() => {
|
||||
return fields.map((field) => ({
|
||||
key: String(field.key),
|
||||
label: field.label,
|
||||
}));
|
||||
}, [fields]);
|
||||
|
||||
// Get filter values for the selected field
|
||||
const filterValues = useMemo(() => {
|
||||
if (!filters.filterField) return [];
|
||||
|
||||
// Get unique values for the selected field
|
||||
const uniqueValues = new Set<string>();
|
||||
|
||||
data.forEach((row) => {
|
||||
const value = row[filters.filterField as keyof typeof row];
|
||||
if (value !== undefined && value !== null) {
|
||||
uniqueValues.add(String(value));
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(uniqueValues).map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}));
|
||||
}, [data, filters.filterField]);
|
||||
|
||||
// Update filters
|
||||
const updateFilters = useCallback((newFilters: Partial<FilterState>) => {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
...newFilters,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Reset filters
|
||||
const resetFilters = useCallback(() => {
|
||||
setFilters({
|
||||
searchText: "",
|
||||
showErrorsOnly: false,
|
||||
filterField: null,
|
||||
filterValue: null,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
filters,
|
||||
filteredData,
|
||||
filterFields,
|
||||
filterValues,
|
||||
updateFilters,
|
||||
resetFilters
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,366 @@
|
||||
import { useCallback } from 'react';
|
||||
import { RowData } from './validationTypes';
|
||||
import type { Field, Fields } from '../../../types';
|
||||
import { ErrorType, ValidationError } from '../../../types';
|
||||
|
||||
export const useRowOperations = <T extends string>(
|
||||
data: RowData<T>[],
|
||||
fields: Fields<T>,
|
||||
setData: React.Dispatch<React.SetStateAction<RowData<T>[]>>,
|
||||
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>,
|
||||
validateFieldFromHook: (value: any, field: Field<T>) => ValidationError[]
|
||||
) => {
|
||||
// Helper function to validate a field value
|
||||
const fieldValidationHelper = useCallback(
|
||||
(rowIndex: number, specificField?: string) => {
|
||||
// Skip validation if row doesn't exist
|
||||
if (rowIndex < 0 || rowIndex >= data.length) return;
|
||||
|
||||
// Get the row data
|
||||
const row = data[rowIndex];
|
||||
|
||||
// If validating a specific field, only check that field
|
||||
if (specificField) {
|
||||
const field = fields.find((f) => String(f.key) === specificField);
|
||||
if (field) {
|
||||
const value = row[specificField as keyof typeof row];
|
||||
|
||||
// Use state setter instead of direct mutation
|
||||
setValidationErrors((prev) => {
|
||||
const newErrors = new Map(prev);
|
||||
const existingErrors = { ...(newErrors.get(rowIndex) || {}) };
|
||||
|
||||
// Quick check for required fields - this prevents flashing errors
|
||||
const isRequired = field.validations?.some(
|
||||
(v) => v.rule === "required"
|
||||
);
|
||||
const isEmpty =
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === "" ||
|
||||
(Array.isArray(value) && value.length === 0) ||
|
||||
(typeof value === "object" &&
|
||||
value !== null &&
|
||||
Object.keys(value).length === 0);
|
||||
|
||||
// For non-empty values, remove required errors immediately
|
||||
if (isRequired && !isEmpty && existingErrors[specificField]) {
|
||||
const nonRequiredErrors = existingErrors[specificField].filter(
|
||||
(e) => e.type !== ErrorType.Required
|
||||
);
|
||||
if (nonRequiredErrors.length === 0) {
|
||||
// If no other errors, remove the field entirely from errors
|
||||
delete existingErrors[specificField];
|
||||
} else {
|
||||
existingErrors[specificField] = nonRequiredErrors;
|
||||
}
|
||||
}
|
||||
|
||||
// Run full validation for the field
|
||||
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||
|
||||
// Update validation errors for this field
|
||||
if (errors.length > 0) {
|
||||
existingErrors[specificField] = errors;
|
||||
} else {
|
||||
delete existingErrors[specificField];
|
||||
}
|
||||
|
||||
// Update validation errors map
|
||||
if (Object.keys(existingErrors).length > 0) {
|
||||
newErrors.set(rowIndex, existingErrors);
|
||||
} else {
|
||||
newErrors.delete(rowIndex);
|
||||
}
|
||||
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Validate all fields in the row
|
||||
setValidationErrors((prev) => {
|
||||
const newErrors = new Map(prev);
|
||||
const rowErrors: Record<string, ValidationError[]> = {};
|
||||
|
||||
fields.forEach((field) => {
|
||||
const fieldKey = String(field.key);
|
||||
const value = row[fieldKey as keyof typeof row];
|
||||
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||
|
||||
if (errors.length > 0) {
|
||||
rowErrors[fieldKey] = errors;
|
||||
}
|
||||
});
|
||||
|
||||
// Update validation errors map
|
||||
if (Object.keys(rowErrors).length > 0) {
|
||||
newErrors.set(rowIndex, rowErrors);
|
||||
} else {
|
||||
newErrors.delete(rowIndex);
|
||||
}
|
||||
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
},
|
||||
[data, fields, validateFieldFromHook, setValidationErrors]
|
||||
);
|
||||
|
||||
// Use validateRow as an alias for fieldValidationHelper for compatibility
|
||||
const validateRow = fieldValidationHelper;
|
||||
|
||||
// Modified updateRow function that properly handles field-specific validation
|
||||
const updateRow = useCallback(
|
||||
(rowIndex: number, key: T, value: any) => {
|
||||
// Process value before updating data
|
||||
let processedValue = value;
|
||||
|
||||
// Strip dollar signs from price fields
|
||||
if (
|
||||
(key === "msrp" || key === "cost_each") &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
processedValue = value.replace(/[$,]/g, "");
|
||||
|
||||
// Also ensure it's a valid number
|
||||
const numValue = parseFloat(processedValue);
|
||||
if (!isNaN(numValue)) {
|
||||
processedValue = numValue.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the row data first
|
||||
const rowData = data[rowIndex];
|
||||
if (!rowData) {
|
||||
console.error(`No row data found for index ${rowIndex}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a copy of the row to avoid mutation
|
||||
const updatedRow = { ...rowData, [key]: processedValue };
|
||||
|
||||
// Update the data immediately - this sets the value
|
||||
setData((prevData) => {
|
||||
const newData = [...prevData];
|
||||
if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||
newData[rowIndex] = updatedRow;
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
|
||||
// Find the field definition
|
||||
const field = fields.find((f) => String(f.key) === key);
|
||||
if (!field) return;
|
||||
|
||||
// CRITICAL FIX: Combine both validation operations into a single state update
|
||||
// to prevent intermediate rendering that causes error icon flashing
|
||||
setValidationErrors((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existingErrors = newMap.get(rowIndex) || {};
|
||||
const newRowErrors = { ...existingErrors };
|
||||
|
||||
// Check for required field first
|
||||
const isRequired = field.validations?.some(
|
||||
(v) => v.rule === "required"
|
||||
);
|
||||
const isEmpty =
|
||||
processedValue === undefined ||
|
||||
processedValue === null ||
|
||||
processedValue === "" ||
|
||||
(Array.isArray(processedValue) && processedValue.length === 0) ||
|
||||
(typeof processedValue === "object" &&
|
||||
processedValue !== null &&
|
||||
Object.keys(processedValue).length === 0);
|
||||
|
||||
// For required fields with values, remove required errors
|
||||
if (isRequired && !isEmpty && newRowErrors[key as string]) {
|
||||
const hasRequiredError = newRowErrors[key as string].some(
|
||||
(e) => e.type === ErrorType.Required
|
||||
);
|
||||
|
||||
if (hasRequiredError) {
|
||||
// Remove required errors but keep other types of errors
|
||||
const nonRequiredErrors = newRowErrors[key as string].filter(
|
||||
(e) => e.type !== ErrorType.Required
|
||||
);
|
||||
|
||||
if (nonRequiredErrors.length === 0) {
|
||||
// If no other errors, delete the field's errors entirely
|
||||
delete newRowErrors[key as string];
|
||||
} else {
|
||||
// Otherwise keep non-required errors
|
||||
newRowErrors[key as string] = nonRequiredErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now run full validation for the field (except for required which we already handled)
|
||||
const errors = validateFieldFromHook(
|
||||
processedValue,
|
||||
field as unknown as Field<T>
|
||||
).filter((e) => e.type !== ErrorType.Required || isEmpty);
|
||||
|
||||
// Update with new validation results
|
||||
if (errors.length > 0) {
|
||||
newRowErrors[key as string] = errors;
|
||||
} else if (!newRowErrors[key as string]) {
|
||||
// If no errors found and no existing errors, ensure field is removed from errors
|
||||
delete newRowErrors[key as string];
|
||||
}
|
||||
|
||||
// Update the map
|
||||
if (Object.keys(newRowErrors).length > 0) {
|
||||
newMap.set(rowIndex, newRowErrors);
|
||||
} else {
|
||||
newMap.delete(rowIndex);
|
||||
}
|
||||
|
||||
return newMap;
|
||||
});
|
||||
|
||||
// Handle simple secondary effects here
|
||||
setTimeout(() => {
|
||||
// Use __index to find the actual row in the full data array
|
||||
const rowId = rowData.__index;
|
||||
|
||||
// Handle company change - clear line/subline
|
||||
if (key === "company" && processedValue) {
|
||||
// Clear any existing line/subline values
|
||||
setData((prevData) => {
|
||||
const newData = [...prevData];
|
||||
const idx = newData.findIndex((item) => item.__index === rowId);
|
||||
if (idx >= 0) {
|
||||
newData[idx] = {
|
||||
...newData[idx],
|
||||
line: undefined,
|
||||
subline: undefined,
|
||||
};
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle line change - clear subline
|
||||
if (key === "line" && processedValue) {
|
||||
// Clear any existing subline value
|
||||
setData((prevData) => {
|
||||
const newData = [...prevData];
|
||||
const idx = newData.findIndex((item) => item.__index === rowId);
|
||||
if (idx >= 0) {
|
||||
newData[idx] = {
|
||||
...newData[idx],
|
||||
subline: undefined,
|
||||
};
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
[data, fields, validateFieldFromHook, setData, setValidationErrors]
|
||||
);
|
||||
|
||||
// Improved revalidateRows function
|
||||
const revalidateRows = useCallback(
|
||||
async (
|
||||
rowIndexes: number[],
|
||||
updatedFields?: { [rowIndex: number]: string[] }
|
||||
) => {
|
||||
// Process all specified rows using a single state update to avoid race conditions
|
||||
setValidationErrors((prev) => {
|
||||
const newErrors = new Map(prev);
|
||||
|
||||
// Process each row
|
||||
for (const rowIndex of rowIndexes) {
|
||||
if (rowIndex < 0 || rowIndex >= data.length) continue;
|
||||
|
||||
const row = data[rowIndex];
|
||||
if (!row) continue;
|
||||
|
||||
// If we have specific fields to update for this row
|
||||
const fieldsToValidate = updatedFields?.[rowIndex] || [];
|
||||
|
||||
if (fieldsToValidate.length > 0) {
|
||||
// Get existing errors for this row
|
||||
const existingRowErrors = { ...(newErrors.get(rowIndex) || {}) };
|
||||
|
||||
// Validate each specified field
|
||||
for (const fieldKey of fieldsToValidate) {
|
||||
const field = fields.find((f) => String(f.key) === fieldKey);
|
||||
if (!field) continue;
|
||||
|
||||
const value = row[fieldKey as keyof typeof row];
|
||||
|
||||
// Run validation for this field
|
||||
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||
|
||||
// Update errors for this field
|
||||
if (errors.length > 0) {
|
||||
existingRowErrors[fieldKey] = errors;
|
||||
} else {
|
||||
delete existingRowErrors[fieldKey];
|
||||
}
|
||||
}
|
||||
|
||||
// Update the row's errors
|
||||
if (Object.keys(existingRowErrors).length > 0) {
|
||||
newErrors.set(rowIndex, existingRowErrors);
|
||||
} else {
|
||||
newErrors.delete(rowIndex);
|
||||
}
|
||||
} else {
|
||||
// No specific fields provided - validate the entire row
|
||||
const rowErrors: Record<string, ValidationError[]> = {};
|
||||
|
||||
// Validate all fields in the row
|
||||
for (const field of fields) {
|
||||
const fieldKey = String(field.key);
|
||||
const value = row[fieldKey as keyof typeof row];
|
||||
|
||||
// Run validation for this field
|
||||
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||
|
||||
// Update errors for this field
|
||||
if (errors.length > 0) {
|
||||
rowErrors[fieldKey] = errors;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the row's errors
|
||||
if (Object.keys(rowErrors).length > 0) {
|
||||
newErrors.set(rowIndex, rowErrors);
|
||||
} else {
|
||||
newErrors.delete(rowIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newErrors;
|
||||
});
|
||||
},
|
||||
[data, fields, validateFieldFromHook]
|
||||
);
|
||||
|
||||
// Copy a cell value to all cells below it in the same column
|
||||
const copyDown = useCallback(
|
||||
(rowIndex: number, key: T) => {
|
||||
// Get the source value to copy
|
||||
const sourceValue = data[rowIndex][key];
|
||||
|
||||
// Update all rows below with the same value using the existing updateRow function
|
||||
// This ensures all validation logic runs consistently
|
||||
for (let i = rowIndex + 1; i < data.length; i++) {
|
||||
// Just use updateRow which will handle validation with proper timing
|
||||
updateRow(i, key, sourceValue);
|
||||
}
|
||||
},
|
||||
[data, updateRow]
|
||||
);
|
||||
|
||||
return {
|
||||
validateRow,
|
||||
updateRow,
|
||||
revalidateRows,
|
||||
copyDown
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,354 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Template, RowData, TemplateState, getApiUrl } from './validationTypes';
|
||||
import { RowSelectionState } from '@tanstack/react-table';
|
||||
import { ValidationError } from '../../../types';
|
||||
|
||||
export const useTemplateManagement = <T extends string>(
|
||||
data: RowData<T>[],
|
||||
setData: React.Dispatch<React.SetStateAction<RowData<T>[]>>,
|
||||
rowSelection: RowSelectionState,
|
||||
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>,
|
||||
setRowValidationStatus: React.Dispatch<React.SetStateAction<Map<number, "pending" | "validating" | "validated" | "error">>>,
|
||||
validateRow: (rowIndex: number, specificField?: string) => void,
|
||||
isApplyingTemplateRef: React.MutableRefObject<boolean>
|
||||
) => {
|
||||
// Template state
|
||||
const [templates, setTemplates] = useState<Template[]>([]);
|
||||
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
|
||||
const [templateState, setTemplateState] = useState<TemplateState>({
|
||||
selectedTemplateId: null,
|
||||
showSaveTemplateDialog: false,
|
||||
newTemplateName: "",
|
||||
newTemplateType: "",
|
||||
});
|
||||
|
||||
// Load templates
|
||||
const loadTemplates = useCallback(async () => {
|
||||
try {
|
||||
setIsLoadingTemplates(true);
|
||||
console.log("Fetching templates from:", `${getApiUrl()}/templates`);
|
||||
const response = await fetch(`${getApiUrl()}/templates`);
|
||||
if (!response.ok) throw new Error("Failed to fetch templates");
|
||||
const templateData = await response.json();
|
||||
const validTemplates = templateData.filter(
|
||||
(t: any) =>
|
||||
t && typeof t === "object" && t.id && t.company && t.product_type
|
||||
);
|
||||
setTemplates(validTemplates);
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
toast.error("Failed to load templates");
|
||||
} finally {
|
||||
setIsLoadingTemplates(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Refresh templates
|
||||
const refreshTemplates = useCallback(() => {
|
||||
loadTemplates();
|
||||
}, [loadTemplates]);
|
||||
|
||||
// Save a new template
|
||||
const saveTemplate = useCallback(
|
||||
async (name: string, type: string) => {
|
||||
try {
|
||||
// Get selected rows
|
||||
const selectedRowIndex = Number(Object.keys(rowSelection)[0]);
|
||||
const selectedRow = data[selectedRowIndex];
|
||||
|
||||
if (!selectedRow) {
|
||||
toast.error("Please select a row to create a template");
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract data for template, removing metadata fields
|
||||
const {
|
||||
__index,
|
||||
__template,
|
||||
__original,
|
||||
__corrected,
|
||||
__changes,
|
||||
...templateData
|
||||
} = selectedRow as any;
|
||||
|
||||
// Clean numeric values (remove $ from price fields)
|
||||
const cleanedData: Record<string, any> = {};
|
||||
|
||||
// Process each key-value pair
|
||||
Object.entries(templateData).forEach(([key, value]) => {
|
||||
// Handle numeric values with dollar signs
|
||||
if (typeof value === "string" && value.includes("$")) {
|
||||
cleanedData[key] = value.replace(/[$,\s]/g, "").trim();
|
||||
}
|
||||
// Handle array values (like categories or ship_restrictions)
|
||||
else if (Array.isArray(value)) {
|
||||
cleanedData[key] = value;
|
||||
}
|
||||
// Handle other values
|
||||
else {
|
||||
cleanedData[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Send the template to the API
|
||||
const response = await fetch(`${getApiUrl()}/templates`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...cleanedData,
|
||||
company: name,
|
||||
product_type: type,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
errorData.error || errorData.details || "Failed to save template"
|
||||
);
|
||||
}
|
||||
|
||||
// Get the new template from the response
|
||||
const newTemplate = await response.json();
|
||||
|
||||
// Update the templates list with the new template
|
||||
setTemplates((prev) => [...prev, newTemplate]);
|
||||
|
||||
// Update the row to show it's using this template
|
||||
setData((prev) => {
|
||||
const newData = [...prev];
|
||||
if (newData[selectedRowIndex]) {
|
||||
newData[selectedRowIndex] = {
|
||||
...newData[selectedRowIndex],
|
||||
__template: newTemplate.id.toString(),
|
||||
};
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
|
||||
toast.success(`Template "${name}" saved successfully`);
|
||||
|
||||
// Reset dialog state
|
||||
setTemplateState((prev) => ({
|
||||
...prev,
|
||||
showSaveTemplateDialog: false,
|
||||
newTemplateName: "",
|
||||
newTemplateType: "",
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error saving template:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to save template"
|
||||
);
|
||||
}
|
||||
},
|
||||
[data, rowSelection, setData]
|
||||
);
|
||||
|
||||
// Apply template to rows - optimized version
|
||||
const applyTemplate = useCallback(
|
||||
(templateId: string, rowIndexes: number[]) => {
|
||||
const template = templates.find((t) => t.id.toString() === templateId);
|
||||
|
||||
if (!template) {
|
||||
toast.error("Template not found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Applying template ${templateId} to rows:`, rowIndexes);
|
||||
|
||||
// Validate row indexes
|
||||
const validRowIndexes = rowIndexes.filter(
|
||||
(index) => index >= 0 && index < data.length && Number.isInteger(index)
|
||||
);
|
||||
|
||||
if (validRowIndexes.length === 0) {
|
||||
toast.error("No valid rows to update");
|
||||
console.error("Invalid row indexes:", rowIndexes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the template application flag
|
||||
isApplyingTemplateRef.current = true;
|
||||
|
||||
// Save scroll position
|
||||
const scrollPosition = {
|
||||
left: window.scrollX,
|
||||
top: window.scrollY,
|
||||
};
|
||||
|
||||
// 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, ValidationError[]>>();
|
||||
const batchStatuses = new Map<
|
||||
number,
|
||||
"pending" | "validating" | "validated" | "error"
|
||||
>();
|
||||
|
||||
// Extract template fields once outside the loop
|
||||
const templateFields = Object.entries(template).filter(
|
||||
([key]) =>
|
||||
![
|
||||
"id",
|
||||
"__meta",
|
||||
"__template",
|
||||
"__original",
|
||||
"__corrected",
|
||||
"__changes",
|
||||
].includes(key)
|
||||
);
|
||||
|
||||
// Apply template to each valid row
|
||||
validRowIndexes.forEach((index) => {
|
||||
// Create a new row with template values
|
||||
const originalRow = newData[index];
|
||||
const updatedRow = { ...originalRow } as Record<string, any>;
|
||||
|
||||
// Apply template fields (excluding metadata fields)
|
||||
for (const [key, value] of templateFields) {
|
||||
updatedRow[key] = value;
|
||||
}
|
||||
|
||||
// Mark the row as using this template
|
||||
updatedRow.__template = templateId;
|
||||
|
||||
// Update the row in the data array
|
||||
newData[index] = updatedRow as RowData<T>;
|
||||
|
||||
// Clear validation errors and mark as validated
|
||||
batchErrors.set(index, {});
|
||||
batchStatuses.set(index, "validated");
|
||||
});
|
||||
|
||||
// Perform a single update for all rows
|
||||
setData(newData);
|
||||
|
||||
// Update all validation errors and statuses at once
|
||||
setValidationErrors((prev) => {
|
||||
const newErrors = new Map(prev);
|
||||
for (const [rowIndex, errors] of batchErrors.entries()) {
|
||||
newErrors.set(rowIndex, errors);
|
||||
}
|
||||
return newErrors;
|
||||
});
|
||||
|
||||
setRowValidationStatus((prev) => {
|
||||
const newStatus = new Map(prev);
|
||||
for (const [rowIndex, status] of batchStatuses.entries()) {
|
||||
newStatus.set(rowIndex, status);
|
||||
}
|
||||
return newStatus;
|
||||
});
|
||||
|
||||
// Restore scroll position
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo(scrollPosition.left, scrollPosition.top);
|
||||
});
|
||||
|
||||
// Show success toast
|
||||
if (validRowIndexes.length === 1) {
|
||||
toast.success("Template applied");
|
||||
} else if (validRowIndexes.length > 1) {
|
||||
toast.success(`Template applied to ${validRowIndexes.length} rows`);
|
||||
}
|
||||
|
||||
// Check which rows need UPC validation
|
||||
const upcValidationRows = validRowIndexes.filter((rowIndex) => {
|
||||
const row = newData[rowIndex];
|
||||
return row && row.upc && row.supplier;
|
||||
});
|
||||
|
||||
// Validate UPCs for rows that have both UPC and supplier
|
||||
if (upcValidationRows.length > 0) {
|
||||
console.log(
|
||||
`Validating UPCs for ${upcValidationRows.length} rows after template application`
|
||||
);
|
||||
|
||||
// Schedule UPC validation for the next tick to allow UI to update first
|
||||
setTimeout(() => {
|
||||
upcValidationRows.forEach((rowIndex) => {
|
||||
const row = newData[rowIndex];
|
||||
if (row && row.upc && row.supplier) {
|
||||
validateRow(rowIndex);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Reset the template application flag
|
||||
isApplyingTemplateRef.current = false;
|
||||
},
|
||||
[
|
||||
data,
|
||||
templates,
|
||||
setData,
|
||||
setValidationErrors,
|
||||
setRowValidationStatus,
|
||||
validateRow,
|
||||
]
|
||||
);
|
||||
|
||||
// Apply template to selected rows
|
||||
const applyTemplateToSelected = useCallback(
|
||||
(templateId: string) => {
|
||||
if (!templateId) return;
|
||||
|
||||
// Update the selected template ID
|
||||
setTemplateState((prev) => ({
|
||||
...prev,
|
||||
selectedTemplateId: templateId,
|
||||
}));
|
||||
|
||||
// Get selected row keys (which may be UUIDs)
|
||||
const selectedKeys = Object.entries(rowSelection)
|
||||
.filter(([_, selected]) => selected === true)
|
||||
.map(([key, _]) => key);
|
||||
|
||||
console.log("Selected row keys:", selectedKeys);
|
||||
|
||||
if (selectedKeys.length === 0) {
|
||||
toast.error("No rows selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Map UUID keys to array indices
|
||||
const selectedIndexes = selectedKeys
|
||||
.map((key) => {
|
||||
// Find the matching row index in the data array
|
||||
const index = data.findIndex(
|
||||
(row) =>
|
||||
(row.__index && row.__index === key) || // Match by __index
|
||||
String(data.indexOf(row)) === key // Or by numeric index
|
||||
);
|
||||
return index;
|
||||
})
|
||||
.filter((index) => index !== -1); // Filter out any not found
|
||||
|
||||
console.log("Mapped row indices:", selectedIndexes);
|
||||
|
||||
if (selectedIndexes.length === 0) {
|
||||
toast.error("Could not find selected rows");
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply template to selected rows
|
||||
applyTemplate(templateId, selectedIndexes);
|
||||
},
|
||||
[rowSelection, applyTemplate, setTemplateState, data]
|
||||
);
|
||||
|
||||
return {
|
||||
templates,
|
||||
isLoadingTemplates,
|
||||
templateState,
|
||||
setTemplateState,
|
||||
loadTemplates,
|
||||
refreshTemplates,
|
||||
saveTemplate,
|
||||
applyTemplate,
|
||||
applyTemplateToSelected
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useCallback } from 'react';
|
||||
import { RowData } from './validationTypes';
|
||||
import type { Fields } from '../../../types';
|
||||
import { ErrorSources, ErrorType, ValidationError } from '../../../types';
|
||||
|
||||
export const useUniqueItemNumbersValidation = <T extends string>(
|
||||
data: RowData<T>[],
|
||||
fields: Fields<T>,
|
||||
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>
|
||||
) => {
|
||||
// Update validateUniqueItemNumbers to also check for uniqueness of UPC/barcode
|
||||
const validateUniqueItemNumbers = useCallback(async () => {
|
||||
console.log("Validating unique fields");
|
||||
|
||||
// Skip if no data
|
||||
if (!data.length) return;
|
||||
|
||||
// Track unique identifiers in maps
|
||||
const uniqueFieldsMap = new Map<string, Map<string, number[]>>();
|
||||
|
||||
// Find fields that need uniqueness validation
|
||||
const uniqueFields = fields
|
||||
.filter((field) => field.validations?.some((v) => v.rule === "unique"))
|
||||
.map((field) => String(field.key));
|
||||
|
||||
console.log(
|
||||
`Found ${uniqueFields.length} fields requiring uniqueness validation:`,
|
||||
uniqueFields
|
||||
);
|
||||
|
||||
// Always check item_number uniqueness even if not explicitly defined
|
||||
if (!uniqueFields.includes("item_number")) {
|
||||
uniqueFields.push("item_number");
|
||||
}
|
||||
|
||||
// Initialize maps for each unique field
|
||||
uniqueFields.forEach((fieldKey) => {
|
||||
uniqueFieldsMap.set(fieldKey, new Map<string, number[]>());
|
||||
});
|
||||
|
||||
// Initialize batch updates
|
||||
const errors = new Map<number, Record<string, ValidationError[]>>();
|
||||
|
||||
// Single pass through data to identify all unique values
|
||||
data.forEach((row, index) => {
|
||||
uniqueFields.forEach((fieldKey) => {
|
||||
const value = row[fieldKey as keyof typeof row];
|
||||
|
||||
// Skip empty values
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueStr = String(value);
|
||||
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
||||
|
||||
if (fieldMap) {
|
||||
// Get or initialize the array of indices for this value
|
||||
const indices = fieldMap.get(valueStr) || [];
|
||||
indices.push(index);
|
||||
fieldMap.set(valueStr, indices);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Process duplicates
|
||||
uniqueFields.forEach((fieldKey) => {
|
||||
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
||||
if (!fieldMap) return;
|
||||
|
||||
fieldMap.forEach((indices, value) => {
|
||||
// Only process if there are duplicates
|
||||
if (indices.length > 1) {
|
||||
// Get the validation rule for this field
|
||||
const field = fields.find((f) => String(f.key) === fieldKey);
|
||||
const validationRule = field?.validations?.find(
|
||||
(v) => v.rule === "unique"
|
||||
);
|
||||
|
||||
const errorObj = {
|
||||
message:
|
||||
validationRule?.errorMessage || `Duplicate ${fieldKey}: ${value}`,
|
||||
level: validationRule?.level || ("error" as "error"),
|
||||
source: ErrorSources.Table,
|
||||
type: ErrorType.Unique,
|
||||
};
|
||||
|
||||
// Add error to each row with this value
|
||||
indices.forEach((rowIndex) => {
|
||||
const rowErrors = errors.get(rowIndex) || {};
|
||||
rowErrors[fieldKey] = [errorObj];
|
||||
errors.set(rowIndex, rowErrors);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Apply batch updates only if we have errors to report
|
||||
if (errors.size > 0) {
|
||||
// OPTIMIZATION: Check if we actually have new errors before updating state
|
||||
let hasChanges = false;
|
||||
|
||||
// We'll update errors with a single batch operation
|
||||
setValidationErrors((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
|
||||
// Check each row for changes
|
||||
errors.forEach((rowErrors, rowIndex) => {
|
||||
const existingErrors = newMap.get(rowIndex) || {};
|
||||
const updatedErrors = { ...existingErrors };
|
||||
let rowHasChanges = false;
|
||||
|
||||
// Check each field for changes
|
||||
Object.entries(rowErrors).forEach(([fieldKey, fieldErrors]) => {
|
||||
// Compare with existing errors
|
||||
const existingFieldErrors = existingErrors[fieldKey];
|
||||
|
||||
if (
|
||||
!existingFieldErrors ||
|
||||
existingFieldErrors.length !== fieldErrors.length ||
|
||||
!existingFieldErrors.every(
|
||||
(err, idx) =>
|
||||
err.message === fieldErrors[idx].message &&
|
||||
err.type === fieldErrors[idx].type
|
||||
)
|
||||
) {
|
||||
// We have a change
|
||||
updatedErrors[fieldKey] = fieldErrors;
|
||||
rowHasChanges = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Only update if we have changes
|
||||
if (rowHasChanges) {
|
||||
newMap.set(rowIndex, updatedErrors);
|
||||
}
|
||||
});
|
||||
|
||||
// Only return a new map if we have changes
|
||||
return hasChanges ? newMap : prev;
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Uniqueness validation complete");
|
||||
}, [data, fields, setValidationErrors]);
|
||||
|
||||
return {
|
||||
validateUniqueItemNumbers
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { Fields } from '../../../types';
|
||||
import { ErrorSources, ErrorType } from '../../../types';
|
||||
import { RowData, InfoWithSource, isEmpty } from './validationTypes';
|
||||
import { clearValidationCacheForField } from './useFieldValidation';
|
||||
|
||||
export const useUniqueValidation = <T extends string>(
|
||||
fields: Fields<T>
|
||||
) => {
|
||||
// Additional function to explicitly validate uniqueness for specified fields
|
||||
const validateUniqueField = useCallback((data: RowData<T>[], fieldKey: string) => {
|
||||
// Field keys that need special handling for uniqueness
|
||||
const uniquenessFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||
|
||||
// If the field doesn't need uniqueness validation, return empty errors
|
||||
if (!uniquenessFields.includes(fieldKey)) {
|
||||
const field = fields.find(f => String(f.key) === fieldKey);
|
||||
if (!field || !field.validations?.some(v => v.rule === 'unique')) {
|
||||
return new Map<number, Record<string, InfoWithSource>>();
|
||||
}
|
||||
}
|
||||
|
||||
// Create map to track errors
|
||||
const uniqueErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||
|
||||
// Find the field definition
|
||||
const field = fields.find(f => String(f.key) === fieldKey);
|
||||
if (!field) return uniqueErrors;
|
||||
|
||||
// Get validation properties
|
||||
const validation = field.validations?.find(v => v.rule === 'unique');
|
||||
const allowEmpty = validation?.allowEmpty ?? false;
|
||||
const errorMessage = validation?.errorMessage || `${field.label} must be unique`;
|
||||
const level = validation?.level || 'error';
|
||||
|
||||
// Track values for uniqueness check
|
||||
const valueMap = new Map<string, number[]>();
|
||||
|
||||
// Build value map
|
||||
data.forEach((row, rowIndex) => {
|
||||
const value = String(row[fieldKey as keyof typeof row] || '');
|
||||
|
||||
// Skip empty values if allowed
|
||||
if (allowEmpty && isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valueMap.has(value)) {
|
||||
valueMap.set(value, [rowIndex]);
|
||||
} else {
|
||||
valueMap.get(value)?.push(rowIndex);
|
||||
}
|
||||
});
|
||||
|
||||
// Add errors for duplicate values
|
||||
valueMap.forEach((rowIndexes, value) => {
|
||||
if (rowIndexes.length > 1) {
|
||||
// Skip empty values
|
||||
if (!value || value.trim() === '') return;
|
||||
|
||||
// Add error to all duplicate rows
|
||||
rowIndexes.forEach(rowIndex => {
|
||||
// Create errors object if needed
|
||||
if (!uniqueErrors.has(rowIndex)) {
|
||||
uniqueErrors.set(rowIndex, {});
|
||||
}
|
||||
|
||||
// Add error for this field
|
||||
uniqueErrors.get(rowIndex)![fieldKey] = {
|
||||
message: errorMessage,
|
||||
level: level as 'info' | 'warning' | 'error',
|
||||
source: ErrorSources.Table,
|
||||
type: ErrorType.Unique
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return uniqueErrors;
|
||||
}, [fields]);
|
||||
|
||||
// Validate uniqueness for multiple fields
|
||||
const validateUniqueFields = useCallback((data: RowData<T>[], fieldKeys: string[]) => {
|
||||
// Process each field and merge results
|
||||
const allErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||
|
||||
fieldKeys.forEach(fieldKey => {
|
||||
const fieldErrors = validateUniqueField(data, fieldKey);
|
||||
|
||||
// Merge errors
|
||||
fieldErrors.forEach((errors, rowIdx) => {
|
||||
if (!allErrors.has(rowIdx)) {
|
||||
allErrors.set(rowIdx, {});
|
||||
}
|
||||
Object.assign(allErrors.get(rowIdx)!, errors);
|
||||
});
|
||||
});
|
||||
|
||||
return allErrors;
|
||||
}, [validateUniqueField]);
|
||||
|
||||
// Run complete validation for uniqueness
|
||||
const validateAllUniqueFields = useCallback((data: RowData<T>[]) => {
|
||||
// Get fields requiring uniqueness validation
|
||||
const uniqueFields = fields
|
||||
.filter(field => field.validations?.some(v => v.rule === 'unique'))
|
||||
.map(field => String(field.key));
|
||||
|
||||
// Also add standard unique fields that might not be explicitly marked as unique
|
||||
const standardUniqueFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||
|
||||
// Combine all fields that need uniqueness validation
|
||||
const allUniqueFieldKeys = [...new Set([
|
||||
...uniqueFields,
|
||||
...standardUniqueFields
|
||||
])];
|
||||
|
||||
// Filter to only fields that exist in the data
|
||||
const existingFields = allUniqueFieldKeys.filter(fieldKey =>
|
||||
data.some(row => fieldKey in row)
|
||||
);
|
||||
|
||||
// Validate all fields at once
|
||||
return validateUniqueFields(data, existingFields);
|
||||
}, [fields, validateUniqueFields]);
|
||||
|
||||
return {
|
||||
validateUniqueField,
|
||||
validateUniqueFields,
|
||||
validateAllUniqueFields
|
||||
};
|
||||
};
|
||||
@@ -1,248 +1,24 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { Field, Fields, RowHook, TableHook } from '../../../types'
|
||||
import type { Meta } from '../types'
|
||||
import { ErrorSources, ErrorType, ValidationError } from '../../../types'
|
||||
import { RowData } from './useValidationState'
|
||||
|
||||
// Define InfoWithSource to match the expected structure
|
||||
// Make sure source is required (not optional)
|
||||
export interface InfoWithSource {
|
||||
message: string;
|
||||
level: 'info' | 'warning' | 'error';
|
||||
source: ErrorSources;
|
||||
type: ErrorType;
|
||||
}
|
||||
|
||||
// Shared utility function for checking empty values - defined once to avoid duplication
|
||||
const isEmpty = (value: any): boolean =>
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
(Array.isArray(value) && value.length === 0) ||
|
||||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
||||
|
||||
// Create a cache for validation results to avoid repeated validation of the same data
|
||||
const validationResultCache = new Map();
|
||||
|
||||
// Add a function to clear cache for a specific field value
|
||||
export const clearValidationCacheForField = (fieldKey: string) => {
|
||||
// Look for entries that match this field key
|
||||
validationResultCache.forEach((_, key) => {
|
||||
if (key.startsWith(`${fieldKey}-`)) {
|
||||
validationResultCache.delete(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add a special function to clear all uniqueness validation caches
|
||||
export const clearAllUniquenessCaches = () => {
|
||||
// Clear cache for common unique fields
|
||||
['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'].forEach(fieldKey => {
|
||||
clearValidationCacheForField(fieldKey);
|
||||
});
|
||||
|
||||
// Also clear any cache entries that might involve uniqueness validation
|
||||
validationResultCache.forEach((_, key) => {
|
||||
if (key.includes('unique')) {
|
||||
validationResultCache.delete(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
import { ErrorSources } from '../../../types'
|
||||
import { RowData, InfoWithSource } from './validationTypes'
|
||||
import { useFieldValidation, clearValidationCacheForField, clearAllUniquenessCaches } from './useFieldValidation'
|
||||
import { useUniqueValidation } from './useUniqueValidation'
|
||||
|
||||
// Main validation hook that brings together field and uniqueness validation
|
||||
export const useValidation = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
rowHook?: RowHook<T>,
|
||||
tableHook?: TableHook<T>
|
||||
) => {
|
||||
// Validate a single field
|
||||
const validateField = useCallback((
|
||||
value: any,
|
||||
field: Field<T>
|
||||
): ValidationError[] => {
|
||||
const errors: ValidationError[] = []
|
||||
|
||||
if (!field.validations) return errors
|
||||
|
||||
// Create a cache key using field key, value, and validation rules
|
||||
const cacheKey = `${field.key}-${String(value)}-${JSON.stringify(field.validations)}`;
|
||||
|
||||
// Check cache first to avoid redundant validation
|
||||
if (validationResultCache.has(cacheKey)) {
|
||||
return validationResultCache.get(cacheKey) || [];
|
||||
}
|
||||
|
||||
field.validations.forEach(validation => {
|
||||
switch (validation.rule) {
|
||||
case 'required':
|
||||
// Use the shared isEmpty function
|
||||
if (isEmpty(value)) {
|
||||
errors.push({
|
||||
message: validation.errorMessage || 'This field is required',
|
||||
level: validation.level || 'error',
|
||||
type: ErrorType.Required
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'unique':
|
||||
// Unique validation happens at table level, not here
|
||||
break
|
||||
|
||||
case 'regex':
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
try {
|
||||
const regex = new RegExp(validation.value, validation.flags)
|
||||
if (!regex.test(String(value))) {
|
||||
errors.push({
|
||||
message: validation.errorMessage,
|
||||
level: validation.level || 'error',
|
||||
type: ErrorType.Regex
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invalid regex in validation:', error)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Store results in cache to speed up future validations
|
||||
validationResultCache.set(cacheKey, errors);
|
||||
|
||||
return errors
|
||||
}, [])
|
||||
|
||||
// Validate a single row
|
||||
const validateRow = useCallback(async (
|
||||
row: RowData<T>,
|
||||
rowIndex: number,
|
||||
allRows: RowData<T>[]
|
||||
): Promise<Meta> => {
|
||||
// Run field-level validations
|
||||
const fieldErrors: Record<string, ValidationError[]> = {}
|
||||
|
||||
// Use the shared isEmpty function
|
||||
|
||||
fields.forEach(field => {
|
||||
const value = row[String(field.key) as keyof typeof row]
|
||||
const errors = validateField(value, field as Field<T>)
|
||||
|
||||
if (errors.length > 0) {
|
||||
fieldErrors[String(field.key)] = errors
|
||||
}
|
||||
})
|
||||
|
||||
// Special validation for supplier and company fields - only apply if the field exists in fields
|
||||
if (fields.some(field => String(field.key) === 'supplier') && isEmpty(row.supplier)) {
|
||||
fieldErrors['supplier'] = [{
|
||||
message: 'Supplier is required',
|
||||
level: 'error',
|
||||
type: ErrorType.Required
|
||||
}]
|
||||
}
|
||||
|
||||
if (fields.some(field => String(field.key) === 'company') && isEmpty(row.company)) {
|
||||
fieldErrors['company'] = [{
|
||||
message: 'Company is required',
|
||||
level: 'error',
|
||||
type: ErrorType.Required
|
||||
}]
|
||||
}
|
||||
|
||||
// Run row hook if provided
|
||||
let rowHookResult: Meta = {
|
||||
__index: row.__index || String(rowIndex)
|
||||
}
|
||||
if (rowHook) {
|
||||
try {
|
||||
// Call the row hook and extract only the __index property
|
||||
const result = await rowHook(row, rowIndex, allRows);
|
||||
rowHookResult.__index = result.__index || rowHookResult.__index;
|
||||
} catch (error) {
|
||||
console.error('Error in row hook:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// We no longer need to merge errors since we're not storing them in the row data
|
||||
// The calling code should handle storing errors in the validationErrors Map
|
||||
|
||||
return {
|
||||
__index: row.__index || String(rowIndex)
|
||||
}
|
||||
}, [fields, validateField, rowHook])
|
||||
|
||||
// Additional function to explicitly validate uniqueness for specified fields
|
||||
const validateUniqueField = useCallback((data: RowData<T>[], fieldKey: string) => {
|
||||
// Field keys that need special handling for uniqueness
|
||||
const uniquenessFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||
|
||||
// If the field doesn't need uniqueness validation, return empty errors
|
||||
if (!uniquenessFields.includes(fieldKey)) {
|
||||
const field = fields.find(f => String(f.key) === fieldKey);
|
||||
if (!field || !field.validations?.some(v => v.rule === 'unique')) {
|
||||
return new Map<number, Record<string, InfoWithSource>>();
|
||||
}
|
||||
}
|
||||
|
||||
// Create map to track errors
|
||||
const uniqueErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||
|
||||
// Find the field definition
|
||||
const field = fields.find(f => String(f.key) === fieldKey);
|
||||
if (!field) return uniqueErrors;
|
||||
|
||||
// Get validation properties
|
||||
const validation = field.validations?.find(v => v.rule === 'unique');
|
||||
const allowEmpty = validation?.allowEmpty ?? false;
|
||||
const errorMessage = validation?.errorMessage || `${field.label} must be unique`;
|
||||
const level = validation?.level || 'error';
|
||||
|
||||
// Track values for uniqueness check
|
||||
const valueMap = new Map<string, number[]>();
|
||||
|
||||
// Build value map
|
||||
data.forEach((row, rowIndex) => {
|
||||
const value = String(row[fieldKey as keyof typeof row] || '');
|
||||
|
||||
// Skip empty values if allowed
|
||||
if (allowEmpty && isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valueMap.has(value)) {
|
||||
valueMap.set(value, [rowIndex]);
|
||||
} else {
|
||||
valueMap.get(value)?.push(rowIndex);
|
||||
}
|
||||
});
|
||||
|
||||
// Add errors for duplicate values
|
||||
valueMap.forEach((rowIndexes, value) => {
|
||||
if (rowIndexes.length > 1) {
|
||||
// Skip empty values
|
||||
if (!value || value.trim() === '') return;
|
||||
|
||||
// Add error to all duplicate rows
|
||||
rowIndexes.forEach(rowIndex => {
|
||||
// Create errors object if needed
|
||||
if (!uniqueErrors.has(rowIndex)) {
|
||||
uniqueErrors.set(rowIndex, {});
|
||||
}
|
||||
|
||||
// Add error for this field
|
||||
uniqueErrors.get(rowIndex)![fieldKey] = {
|
||||
message: errorMessage,
|
||||
level: level as 'info' | 'warning' | 'error',
|
||||
source: ErrorSources.Table,
|
||||
type: ErrorType.Unique
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return uniqueErrors;
|
||||
}, [fields]);
|
||||
// Use the field validation hook
|
||||
const { validateField, validateRow } = useFieldValidation(fields, rowHook, tableHook);
|
||||
|
||||
// Use the uniqueness validation hook
|
||||
const {
|
||||
validateUniqueField,
|
||||
validateAllUniqueFields
|
||||
} = useUniqueValidation(fields);
|
||||
|
||||
// Run complete validation
|
||||
const validateData = useCallback(async (data: RowData<T>[], fieldToUpdate?: { rowIndex: number, fieldKey: string }) => {
|
||||
@@ -341,9 +117,6 @@ export const useValidation = <T extends string>(
|
||||
// Full validation - all fields for all rows
|
||||
console.log('Running full validation for all fields and rows');
|
||||
|
||||
// Clear validation cache for full validation
|
||||
validationResultCache.clear();
|
||||
|
||||
// Process each row for field-level validations
|
||||
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||
const row = data[rowIndex];
|
||||
@@ -371,38 +144,15 @@ export const useValidation = <T extends string>(
|
||||
}
|
||||
}
|
||||
|
||||
// Get fields requiring uniqueness validation
|
||||
const uniqueFields = fields.filter(field =>
|
||||
field.validations?.some(v => v.rule === 'unique')
|
||||
);
|
||||
// Validate all unique fields
|
||||
const uniqueErrors = validateAllUniqueFields(data);
|
||||
|
||||
// Also add standard unique fields that might not be explicitly marked as unique
|
||||
const standardUniqueFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||
|
||||
// Combine all fields that need uniqueness validation
|
||||
const allUniqueFieldKeys = new Set([
|
||||
...uniqueFields.map(field => String(field.key)),
|
||||
...standardUniqueFields
|
||||
]);
|
||||
|
||||
// Log uniqueness validation fields
|
||||
console.log('Validating unique fields:', Array.from(allUniqueFieldKeys));
|
||||
|
||||
// Run uniqueness validation for each unique field
|
||||
allUniqueFieldKeys.forEach(fieldKey => {
|
||||
// Check if this field exists in the data
|
||||
const hasField = data.some(row => fieldKey in row);
|
||||
if (!hasField) return;
|
||||
|
||||
const uniqueErrors = validateUniqueField(data, fieldKey);
|
||||
|
||||
// Add unique errors to validation errors
|
||||
uniqueErrors.forEach((errors, rowIdx) => {
|
||||
if (!validationErrors.has(rowIdx)) {
|
||||
validationErrors.set(rowIdx, {});
|
||||
}
|
||||
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||
});
|
||||
// Merge in unique errors
|
||||
uniqueErrors.forEach((errors, rowIdx) => {
|
||||
if (!validationErrors.has(rowIdx)) {
|
||||
validationErrors.set(rowIdx, {});
|
||||
}
|
||||
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||
});
|
||||
|
||||
console.log('Uniqueness validation complete');
|
||||
@@ -412,7 +162,7 @@ export const useValidation = <T extends string>(
|
||||
data,
|
||||
validationErrors
|
||||
};
|
||||
}, [fields, validateField, validateUniqueField]);
|
||||
}, [fields, validateField, validateUniqueField, validateAllUniqueFields]);
|
||||
|
||||
return {
|
||||
validateData,
|
||||
@@ -421,5 +171,5 @@ export const useValidation = <T extends string>(
|
||||
validateUniqueField,
|
||||
clearValidationCacheForField,
|
||||
clearAllUniquenessCaches
|
||||
}
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
import type { Data, Field } from "../../../types";
|
||||
import { ErrorSources, ErrorType, ValidationError } from "../../../types";
|
||||
import config from "@/config";
|
||||
import { RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
// Define the Props interface for ValidationStepNew
|
||||
export interface Props<T extends string> {
|
||||
initialData: RowData<T>[];
|
||||
file?: File;
|
||||
onBack?: () => void;
|
||||
onNext?: (data: RowData<T>[]) => void;
|
||||
isFromScratch?: boolean;
|
||||
}
|
||||
|
||||
// Extended Data type with meta information
|
||||
export type RowData<T extends string> = Data<T> & {
|
||||
__index?: string;
|
||||
__template?: string;
|
||||
__original?: Record<string, any>;
|
||||
__corrected?: Record<string, any>;
|
||||
__changes?: Record<string, boolean>;
|
||||
upc?: string;
|
||||
barcode?: string;
|
||||
supplier?: string;
|
||||
company?: string;
|
||||
item_number?: string;
|
||||
[key: string]: any; // Allow any string key for dynamic fields
|
||||
};
|
||||
|
||||
// Template interface
|
||||
export interface Template {
|
||||
id: number;
|
||||
company: string;
|
||||
product_type: string;
|
||||
[key: string]: string | number | boolean | undefined;
|
||||
}
|
||||
|
||||
// Props for the useValidationState hook
|
||||
export interface ValidationStateProps<T extends string> extends Props<T> {}
|
||||
|
||||
// Interface for validation results
|
||||
export interface ValidationResult {
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
data?: Record<string, any>;
|
||||
type?: ErrorType;
|
||||
source?: ErrorSources;
|
||||
}
|
||||
|
||||
// Filter state interface
|
||||
export interface FilterState {
|
||||
searchText: string;
|
||||
showErrorsOnly: boolean;
|
||||
filterField: string | null;
|
||||
filterValue: string | null;
|
||||
}
|
||||
|
||||
// UI validation state interface for useUpcValidation
|
||||
export interface ValidationState {
|
||||
validatingCells: Set<string>; // Using rowIndex-fieldKey as identifier
|
||||
itemNumbers: Map<number, string>; // Using rowIndex as key
|
||||
validatingRows: Set<number>; // Rows currently being validated
|
||||
activeValidations: Set<string>; // Active validations
|
||||
}
|
||||
|
||||
// InfoWithSource interface for validation errors
|
||||
export interface InfoWithSource {
|
||||
message: string;
|
||||
level: 'info' | 'warning' | 'error';
|
||||
source: ErrorSources;
|
||||
type: ErrorType;
|
||||
}
|
||||
|
||||
// Template state interface
|
||||
export interface TemplateState {
|
||||
selectedTemplateId: string | null;
|
||||
showSaveTemplateDialog: boolean;
|
||||
newTemplateName: string;
|
||||
newTemplateType: string;
|
||||
}
|
||||
|
||||
// Add config at the top of the file
|
||||
// Import the config or access it through window
|
||||
declare global {
|
||||
interface Window {
|
||||
config?: {
|
||||
apiUrl: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Use a helper to get API URL consistently
|
||||
export const getApiUrl = () => config.apiUrl;
|
||||
|
||||
// Shared utility function for checking empty values
|
||||
export const isEmpty = (value: any): boolean =>
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
(Array.isArray(value) && value.length === 0) ||
|
||||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ErrorType } from '../types/index'
|
||||
|
||||
/**
|
||||
* Converts an InfoWithSource or similar error object to our Error type
|
||||
* @param error The error object to convert
|
||||
* @returns Our standardized Error object
|
||||
*/
|
||||
export const convertToError = (error: any): ErrorType => {
|
||||
return {
|
||||
message: typeof error.message === 'string' ? error.message : String(error.message || ''),
|
||||
level: error.level || 'error',
|
||||
source: error.source || 'row',
|
||||
type: error.type || 'custom'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely convert an error or array of errors to our Error[] format
|
||||
* @param errors The error or array of errors to convert
|
||||
* @returns Array of our Error objects
|
||||
*/
|
||||
export const convertToErrorArray = (errors: any): ErrorType[] => {
|
||||
if (Array.isArray(errors)) {
|
||||
return errors.map(convertToError)
|
||||
}
|
||||
return [convertToError(errors)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a record of errors to our standardized format
|
||||
* @param errorRecord Record with string keys and error values
|
||||
* @returns Standardized error record
|
||||
*/
|
||||
export const convertErrorRecord = (errorRecord: Record<string, any>): Record<string, ErrorType[]> => {
|
||||
const result: Record<string, ErrorType[]> = {}
|
||||
|
||||
Object.entries(errorRecord).forEach(([key, errors]) => {
|
||||
result[key] = convertToErrorArray(errors)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { toast } from 'sonner'
|
||||
|
||||
/**
|
||||
* Placeholder for validating UPC codes
|
||||
* @param upcValue UPC value to validate
|
||||
* @returns Validation result
|
||||
*/
|
||||
export const validateUpc = async (upcValue: string): Promise<any> => {
|
||||
// Basic validation - UPC should be 12-14 digits
|
||||
if (!/^\d{12,14}$/.test(upcValue)) {
|
||||
toast.error('Invalid UPC format. UPC should be 12-14 digits.')
|
||||
return {
|
||||
error: true,
|
||||
message: 'Invalid UPC format'
|
||||
}
|
||||
}
|
||||
|
||||
// In a real implementation, call an API to validate the UPC
|
||||
// For now, just return a successful result
|
||||
return {
|
||||
error: false,
|
||||
data: {
|
||||
// Mock data that would be returned from the API
|
||||
item_number: `ITEM-${upcValue.substring(0, 6)}`,
|
||||
sku: `SKU-${upcValue.substring(0, 4)}`,
|
||||
description: `Sample Product ${upcValue.substring(0, 4)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an item number for a UPC
|
||||
* @param upcValue UPC value
|
||||
* @returns Generated item number
|
||||
*/
|
||||
export const generateItemNumber = (upcValue: string): string => {
|
||||
// Simple item number generation logic
|
||||
return `ITEM-${upcValue.substring(0, 6)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for handling UPC validation process
|
||||
* @param upcValue UPC value to validate
|
||||
* @param rowIndex Row index being validated
|
||||
* @param updateRow Function to update row data
|
||||
*/
|
||||
export const handleUpcValidation = async (
|
||||
upcValue: string,
|
||||
rowIndex: number,
|
||||
updateRow: (rowIndex: number, key: string, value: any) => void
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Validate the UPC
|
||||
const result = await validateUpc(upcValue)
|
||||
|
||||
if (result.error) {
|
||||
toast.error(result.message || 'UPC validation failed')
|
||||
return
|
||||
}
|
||||
|
||||
// Update row with the validation result data
|
||||
if (result.data) {
|
||||
// Update each field returned from the API
|
||||
Object.entries(result.data).forEach(([key, value]) => {
|
||||
updateRow(rowIndex, key, value)
|
||||
})
|
||||
|
||||
toast.success('UPC validated successfully')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating UPC:', error)
|
||||
toast.error('Failed to validate UPC')
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Helper functions for validation that ensure proper error objects
|
||||
*/
|
||||
|
||||
// Create a standard error object
|
||||
export const createError = (message, level = 'error', source = 'row') => {
|
||||
return { message, level, source };
|
||||
};
|
||||
|
||||
// Convert any error to standard format
|
||||
export const convertError = (error) => {
|
||||
if (!error) return createError('Unknown error');
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return createError(error);
|
||||
}
|
||||
|
||||
return {
|
||||
message: error.message || 'Unknown error',
|
||||
level: error.level || 'error',
|
||||
source: error.source || 'row'
|
||||
};
|
||||
};
|
||||
|
||||
// Convert array of errors or single error to array
|
||||
export const convertToErrorArray = (errors) => {
|
||||
if (Array.isArray(errors)) {
|
||||
return errors.map(convertError);
|
||||
}
|
||||
return [convertError(errors)];
|
||||
};
|
||||
|
||||
// Convert a record of errors to standard format
|
||||
export const convertErrorRecord = (errorRecord) => {
|
||||
const result = {};
|
||||
|
||||
if (!errorRecord) return result;
|
||||
|
||||
Object.entries(errorRecord).forEach(([key, errors]) => {
|
||||
result[key] = convertToErrorArray(errors);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,184 +0,0 @@
|
||||
import { Field, Data, ErrorSources, ErrorType as ValidationErrorType } from '../../../types'
|
||||
import { ErrorType } from '../types/index'
|
||||
|
||||
/**
|
||||
* Formats a price value to a consistent format
|
||||
* @param value The price value to format
|
||||
* @returns The formatted price string
|
||||
*/
|
||||
export const formatPrice = (value: string | number): string => {
|
||||
if (!value) return ''
|
||||
|
||||
// Convert to string and clean
|
||||
const numericValue = String(value).replace(/[^\d.]/g, '')
|
||||
|
||||
// Parse the number
|
||||
const number = parseFloat(numericValue)
|
||||
if (isNaN(number)) return ''
|
||||
|
||||
// Format as currency
|
||||
return number.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a field is a price field
|
||||
* @param field The field to check
|
||||
* @returns True if the field is a price field
|
||||
*/
|
||||
export const isPriceField = (field: Field<any>): boolean => {
|
||||
const fieldType = field.fieldType;
|
||||
return (fieldType.type === 'input' || fieldType.type === 'multi-input') &&
|
||||
'price' in fieldType &&
|
||||
!!fieldType.price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a field is a multi-input type
|
||||
* @param fieldType The field type to check
|
||||
* @returns True if the field is a multi-input type
|
||||
*/
|
||||
export const isMultiInputType = (fieldType: any): boolean => {
|
||||
return fieldType.type === 'multi-input'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the separator for multi-input fields
|
||||
* @param fieldType The field type
|
||||
* @returns The separator string
|
||||
*/
|
||||
export const getMultiInputSeparator = (fieldType: any): string => {
|
||||
if (isMultiInputType(fieldType) && fieldType.separator) {
|
||||
return fieldType.separator
|
||||
}
|
||||
return ','
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs regex validation on a value
|
||||
* @param value The value to validate
|
||||
* @param regex The regex pattern
|
||||
* @param flags Regex flags
|
||||
* @returns True if validation passes
|
||||
*/
|
||||
export const validateRegex = (value: any, regex: string, flags?: string): boolean => {
|
||||
if (value === undefined || value === null || value === '') return true
|
||||
|
||||
try {
|
||||
const regexObj = new RegExp(regex, flags)
|
||||
return regexObj.test(String(value))
|
||||
} catch (error) {
|
||||
console.error('Invalid regex in validation:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a validation error object
|
||||
* @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,
|
||||
type: ValidationErrorType = ValidationErrorType.Custom
|
||||
): ErrorType => {
|
||||
return {
|
||||
message,
|
||||
level,
|
||||
source,
|
||||
type
|
||||
} as ErrorType
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a display value based on field type
|
||||
* @param value The value to format
|
||||
* @param field The field definition
|
||||
* @returns Formatted display value
|
||||
*/
|
||||
export const getDisplayValue = (value: any, field: Field<any>): string => {
|
||||
if (value === undefined || value === null) return ''
|
||||
|
||||
// Handle price fields
|
||||
if (isPriceField(field)) {
|
||||
return formatPrice(value)
|
||||
}
|
||||
|
||||
// Handle multi-input fields
|
||||
if (isMultiInputType(field.fieldType)) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(`${getMultiInputSeparator(field.fieldType)} `)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle boolean values
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'Yes' : 'No'
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates supplier and company fields
|
||||
* @param row The data row
|
||||
* @returns Object with errors for invalid fields
|
||||
*/
|
||||
export const validateSpecialFields = <T extends string>(row: Data<T>): Record<string, ErrorType[]> => {
|
||||
const errors: Record<string, ErrorType[]> = {}
|
||||
|
||||
// Validate supplier field
|
||||
if (!row.supplier) {
|
||||
errors['supplier'] = [{
|
||||
message: 'Supplier is required',
|
||||
level: 'error',
|
||||
source: ErrorSources.Row,
|
||||
type: ValidationErrorType.Required
|
||||
}]
|
||||
}
|
||||
|
||||
// Validate company field
|
||||
if (!row.company) {
|
||||
errors['company'] = [{
|
||||
message: 'Company is required',
|
||||
level: 'error',
|
||||
source: ErrorSources.Row,
|
||||
type: ValidationErrorType.Required
|
||||
}]
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges multiple error objects
|
||||
* @param errors Array of error objects to merge
|
||||
* @returns Merged error object
|
||||
*/
|
||||
export const mergeErrors = (...errors: Record<string, ErrorType[]>[]): Record<string, ErrorType[]> => {
|
||||
const merged: Record<string, ErrorType[]> = {}
|
||||
|
||||
errors.forEach(errorObj => {
|
||||
if (!errorObj) return
|
||||
|
||||
Object.entries(errorObj).forEach(([key, errs]) => {
|
||||
if (!merged[key]) {
|
||||
merged[key] = []
|
||||
}
|
||||
|
||||
merged[key] = [
|
||||
...merged[key],
|
||||
...(Array.isArray(errs) ? errs : [errs as ErrorType])
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
return merged
|
||||
}
|
||||
Reference in New Issue
Block a user