More validation fixes and enhancements

This commit is contained in:
2025-09-06 16:15:00 -04:00
parent 4935cfe3bb
commit 54f55b06a1
4 changed files with 46 additions and 55 deletions

View File

@@ -123,26 +123,40 @@ const ValidationContainer = <T extends string>({
// Function to mark a row for revalidation // Function to mark a row for revalidation
const markRowForRevalidation = useCallback((rowIndex: number, fieldKey?: string) => { const markRowForRevalidation = useCallback((rowIndex: number, fieldKey?: string) => {
// Map filtered rowIndex to original data index via __index
const originalIndex = (() => {
try {
const row = filteredData[rowIndex];
if (!row) return rowIndex;
const id = row.__index;
if (!id) return rowIndex;
const idx = data.findIndex(r => r.__index === id);
return idx >= 0 ? idx : rowIndex;
} catch {
return rowIndex;
}
})();
setFieldsToRevalidate(prev => { setFieldsToRevalidate(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
newSet.add(rowIndex); newSet.add(originalIndex);
return newSet; return newSet;
}); });
// Also track which specific field needs to be revalidated // Also track which specific field needs to be revalidated
if (fieldKey) { if (fieldKey) {
setFieldsToRevalidateMap(prev => { setFieldsToRevalidateMap(prev => {
const newMap = { ...prev }; const newMap = { ...prev };
if (!newMap[rowIndex]) { if (!newMap[originalIndex]) {
newMap[rowIndex] = []; newMap[originalIndex] = [];
} }
if (!newMap[rowIndex].includes(fieldKey)) { if (!newMap[originalIndex].includes(fieldKey)) {
newMap[rowIndex] = [...newMap[rowIndex], fieldKey]; newMap[originalIndex] = [...newMap[originalIndex], fieldKey];
} }
return newMap; return newMap;
}); });
} }
}, []); }, [data, filteredData]);
// Add a ref to track the last validation time // Add a ref to track the last validation time
@@ -488,52 +502,16 @@ const ValidationContainer = <T extends string>({
// Detect if this is a direct item_number edit // Detect if this is a direct item_number edit
const isItemNumberEdit = key === 'item_number' as T; const isItemNumberEdit = key === 'item_number' as T;
// For item_number edits, we need special handling to ensure they persist // For item_number edits, use core updateRow to atomically update + validate
if (isItemNumberEdit) { if (isItemNumberEdit) {
console.log(`Manual edit to item_number: ${value}`); const idx = originalIndex >= 0 ? originalIndex : rowIndex;
validationState.updateRow(idx, key as unknown as any, processedValue);
// First, update data immediately to ensure edit takes effect
setData(prevData => {
const newData = [...prevData];
if (originalIndex >= 0 && originalIndex < newData.length) {
newData[originalIndex] = {
...newData[originalIndex],
[key]: processedValue
};
}
return newData;
});
// Mark for revalidation after a delay to ensure data update completes first
setTimeout(() => {
markRowForRevalidation(rowIndex, key as string);
}, 0);
// Return early to prevent double-updating
return; return;
} }
// For all other fields, use standard approach // For all other fields, use core updateRow for atomic update + validation
// Always use setData for updating - immediate update for better UX const idx = originalIndex >= 0 ? originalIndex : rowIndex;
const updatedRow = { ...rowData, [key]: processedValue }; validationState.updateRow(idx, key as unknown as any, processedValue);
// Mark this row for revalidation to clear any existing errors
markRowForRevalidation(rowIndex, key as string);
// Update the data immediately to show the change
setData(prevData => {
const newData = [...prevData];
if (originalIndex >= 0 && originalIndex < newData.length) {
// Create a new row object with the updated field
newData[originalIndex] = {
...newData[originalIndex],
[key]: processedValue
};
} else {
console.error(`Invalid originalIndex: ${originalIndex}, data length: ${newData.length}`);
}
return newData;
});
// Secondary effects - using requestAnimationFrame for better performance // Secondary effects - using requestAnimationFrame for better performance
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -1225,4 +1203,4 @@ const ValidationContainer = <T extends string>({
) )
} }
export default ValidationContainer export default ValidationContainer

View File

@@ -24,6 +24,9 @@ type ErrorType = {
source?: string; source?: string;
} }
// Stable empty errors array to prevent unnecessary re-renders
const EMPTY_ERRORS: ErrorType[] = Object.freeze([]);
interface ValidationTableProps<T extends string> { interface ValidationTableProps<T extends string> {
data: RowData<T>[] data: RowData<T>[]
fields: Fields<T> fields: Fields<T>
@@ -421,7 +424,8 @@ const ValidationTable = <T extends string>({
} }
// Get validation errors for this cell // Get validation errors for this cell
const cellErrors = validationErrors.get(row.index)?.[fieldKey] || []; // Use stable EMPTY_ERRORS to avoid new array creation on every render
const cellErrors = validationErrors.get(row.index)?.[fieldKey] || EMPTY_ERRORS;
// Create a copy of the field with guaranteed field type for line and subline fields // Create a copy of the field with guaranteed field type for line and subline fields
let fieldWithType = field; let fieldWithType = field;
@@ -702,4 +706,4 @@ const areEqual = (prev: ValidationTableProps<any>, next: ValidationTableProps<an
return true; return true;
}; };
export default React.memo(ValidationTable, areEqual); export default React.memo(ValidationTable, areEqual);

View File

@@ -18,6 +18,15 @@ export const useFilterManagement = <T extends string>(
// Filter data based on current filter state // Filter data based on current filter state
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
// Fast path: no filters active, return original data reference to avoid re-renders
const noSearch = !filters.searchText || filters.searchText.trim() === '';
const noErrorsOnly = !filters.showErrorsOnly;
const noFieldFilter = !filters.filterField || !filters.filterValue || filters.filterValue.trim() === '';
if (noSearch && noErrorsOnly && noFieldFilter) {
return data; // preserve reference; prevents full table rerender on error map changes
}
return data.filter((row, index) => { return data.filter((row, index) => {
// Filter by search text // Filter by search text
if (filters.searchText) { if (filters.searchText) {
@@ -107,4 +116,4 @@ export const useFilterManagement = <T extends string>(
updateFilters, updateFilters,
resetFilters resetFilters
}; };
}; };

View File

@@ -301,8 +301,8 @@ export const useRowOperations = <T extends string>(
// Update with new validation results // Update with new validation results
if (errors.length > 0) { if (errors.length > 0) {
newRowErrors[key as string] = errors; newRowErrors[key as string] = errors;
} else if (!newRowErrors[key as string]) { } else {
// If no errors found and no existing errors, ensure field is removed from errors // Clear any existing errors for this field
delete newRowErrors[key as string]; delete newRowErrors[key as string];
} }