Fix lines/sublines getting stuck in loading state on change
This commit is contained in:
@@ -446,6 +446,13 @@ const ValidationContainer = <T extends string>({
|
||||
line: undefined,
|
||||
subline: undefined
|
||||
};
|
||||
} else if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||
// Fallback if __index is not yet present on the row
|
||||
newData[rowIndex] = {
|
||||
...newData[rowIndex],
|
||||
line: undefined,
|
||||
subline: undefined
|
||||
};
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
@@ -454,7 +461,9 @@ const ValidationContainer = <T extends string>({
|
||||
setValidatingCells(prev => new Set(prev).add(`${rowIndex}-line`));
|
||||
|
||||
setTimeout(() => {
|
||||
fetchProductLines(rowId, companyId)
|
||||
// Use __index when available, otherwise fall back to the UI row index
|
||||
const rowKey = (rowId !== undefined && rowId !== null) ? rowId : rowIndex;
|
||||
fetchProductLines(rowKey, companyId)
|
||||
.catch(err => {
|
||||
console.error(`Error fetching product lines for company ${companyId}:`, err);
|
||||
toast.error("Failed to load product lines");
|
||||
@@ -480,6 +489,12 @@ const ValidationContainer = <T extends string>({
|
||||
...newData[idx],
|
||||
subline: undefined
|
||||
};
|
||||
} else if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||
// Fallback if __index is not yet present on the row
|
||||
newData[rowIndex] = {
|
||||
...newData[rowIndex],
|
||||
subline: undefined
|
||||
};
|
||||
} else {
|
||||
console.warn(`Could not find row with ID ${rowId} to clear subline values`);
|
||||
}
|
||||
@@ -489,7 +504,9 @@ const ValidationContainer = <T extends string>({
|
||||
// Fetch sublines
|
||||
setValidatingCells(prev => new Set(prev).add(`${rowIndex}-subline`));
|
||||
|
||||
fetchSublines(rowId, lineId)
|
||||
// Use __index when available, otherwise fall back to the UI row index
|
||||
const rowKey = (rowId !== undefined && rowId !== null) ? rowId : rowIndex;
|
||||
fetchSublines(rowKey, lineId)
|
||||
.catch(err => {
|
||||
console.error(`Error fetching sublines for line ${lineId}:`, err);
|
||||
toast.error("Failed to load sublines");
|
||||
|
||||
@@ -342,12 +342,13 @@ const ValidationTable = <T extends string>({
|
||||
cell: ({ row }) => {
|
||||
// Get row-specific options for line and subline fields
|
||||
let options = fieldOptions;
|
||||
const rowId = row.original.__index;
|
||||
const rowId = (row.original as any).__index;
|
||||
const lookupKey = (rowId !== undefined && rowId !== null) ? rowId : row.index;
|
||||
|
||||
if (fieldKey === 'line' && rowId && rowProductLines[rowId]) {
|
||||
options = rowProductLines[rowId];
|
||||
} else if (fieldKey === 'subline' && rowId && rowSublines[rowId]) {
|
||||
options = rowSublines[rowId];
|
||||
if (fieldKey === 'line' && lookupKey !== undefined && rowProductLines[lookupKey]) {
|
||||
options = rowProductLines[lookupKey];
|
||||
} else if (fieldKey === 'subline' && lookupKey !== undefined && rowSublines[lookupKey]) {
|
||||
options = rowSublines[lookupKey];
|
||||
}
|
||||
|
||||
// Get the current cell value first
|
||||
@@ -374,10 +375,10 @@ const ValidationTable = <T extends string>({
|
||||
isLoading = true;
|
||||
}
|
||||
// Add loading state for line/subline fields
|
||||
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
||||
else if (fieldKey === 'line' && lookupKey !== undefined && isLoadingLines[lookupKey]) {
|
||||
isLoading = true;
|
||||
}
|
||||
else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
|
||||
else if (fieldKey === 'subline' && lookupKey !== undefined && isLoadingSublines[lookupKey]) {
|
||||
isLoading = true;
|
||||
}
|
||||
}
|
||||
@@ -634,14 +635,21 @@ const ValidationTable = <T extends string>({
|
||||
);
|
||||
};
|
||||
|
||||
// Simplified memo - React 18+ handles most optimizations well
|
||||
// Only check critical props that frequently change
|
||||
// Memo comparator: re-render when any prop affecting visible state changes.
|
||||
// Keep this conservative to avoid skipping updates for loading/options states.
|
||||
const areEqual = (prev: ValidationTableProps<any>, next: ValidationTableProps<any>) => {
|
||||
// Only prevent re-render if absolutely nothing changed (rare case)
|
||||
return (
|
||||
// Core props
|
||||
prev.data === next.data &&
|
||||
prev.validationErrors === next.validationErrors &&
|
||||
prev.rowSelection === next.rowSelection
|
||||
prev.rowSelection === next.rowSelection &&
|
||||
// Loading + validation state that affects cell skeletons
|
||||
prev.validatingCells === next.validatingCells &&
|
||||
prev.isLoadingLines === next.isLoadingLines &&
|
||||
prev.isLoadingSublines === next.isLoadingSublines &&
|
||||
// Options sources used for line/subline selects
|
||||
prev.rowProductLines === next.rowProductLines &&
|
||||
prev.rowSublines === next.rowSublines
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import axios from 'axios'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
@@ -18,24 +18,53 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
const [companyLinesCache, setCompanyLinesCache] = useState<Record<string, any[]>>({});
|
||||
const [lineSublineCache, setLineSublineCache] = useState<Record<string, any[]>>({});
|
||||
|
||||
// Track in-flight requests to prevent duplicate fetches (especially in StrictMode/dev)
|
||||
const pendingCompanyRequests = useRef<Set<string>>(new Set());
|
||||
const pendingLineRequests = useRef<Set<string>>(new Set());
|
||||
|
||||
// Function to fetch product lines for a specific company - memoized
|
||||
const fetchProductLines = useCallback(async (rowIndex: string | number, companyId: string) => {
|
||||
const fetchProductLines = useCallback(async (rowIndex: string | number | undefined | null, companyId: string) => {
|
||||
try {
|
||||
// Only fetch if we have a valid company ID
|
||||
if (!companyId) return;
|
||||
|
||||
console.log(`Fetching product lines for row ${rowIndex}, company ${companyId}`);
|
||||
const logRowKey = (rowIndex !== undefined && rowIndex !== null) ? rowIndex : 'all-matching-rows';
|
||||
console.log(`Fetching product lines for row ${logRowKey}, company ${companyId}`);
|
||||
|
||||
// Check if we already have this company's lines in the cache
|
||||
if (companyLinesCache[companyId]) {
|
||||
console.log(`Using cached product lines for company ${companyId}`);
|
||||
// Use cached data
|
||||
setRowProductLines(prev => ({ ...prev, [rowIndex]: companyLinesCache[companyId] }));
|
||||
return companyLinesCache[companyId];
|
||||
const cached = companyLinesCache[companyId];
|
||||
// Update the specific row if provided
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setRowProductLines(prev => ({ ...prev, [rowIndex]: cached }));
|
||||
}
|
||||
// Also update all rows that currently have this company set
|
||||
const updates: Record<string, any[]> = {};
|
||||
data.forEach((row, idx) => {
|
||||
if (row.company && String(row.company) === String(companyId)) {
|
||||
const key = (row.__index !== undefined && row.__index !== null) ? row.__index : idx;
|
||||
updates[key] = cached;
|
||||
}
|
||||
});
|
||||
if (Object.keys(updates).length > 0) {
|
||||
setRowProductLines(prev => ({ ...prev, ...updates }));
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
// If a request for this company is already in flight, skip duplicate fetch
|
||||
if (pendingCompanyRequests.current.has(companyId)) {
|
||||
console.log(`Skipping fetch for company ${companyId} - request already pending`);
|
||||
return;
|
||||
}
|
||||
pendingCompanyRequests.current.add(companyId);
|
||||
|
||||
// Set loading state for this row
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: true }));
|
||||
}
|
||||
|
||||
// Fetch product lines from API
|
||||
const productLinesUrl = `/api/import/product-lines/${companyId}`;
|
||||
@@ -53,8 +82,22 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
// Store in company cache
|
||||
setCompanyLinesCache(prev => ({ ...prev, [companyId]: formattedLines }));
|
||||
|
||||
// Store for this specific row
|
||||
// Update specific row if provided
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setRowProductLines(prev => ({ ...prev, [rowIndex]: formattedLines }));
|
||||
}
|
||||
|
||||
// Also update all rows that currently have this company set
|
||||
const updates: Record<string, any[]> = {};
|
||||
data.forEach((row, idx) => {
|
||||
if (row.company && String(row.company) === String(companyId)) {
|
||||
const key = (row.__index !== undefined && row.__index !== null) ? row.__index : idx;
|
||||
updates[key] = formattedLines;
|
||||
}
|
||||
});
|
||||
if (Object.keys(updates).length > 0) {
|
||||
setRowProductLines(prev => ({ ...prev, ...updates }));
|
||||
}
|
||||
|
||||
return formattedLines;
|
||||
} catch (error) {
|
||||
@@ -65,33 +108,63 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
|
||||
|
||||
// Store empty array for this specific row
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setRowProductLines(prev => ({ ...prev, [rowIndex]: [] }));
|
||||
}
|
||||
|
||||
return [];
|
||||
} finally {
|
||||
// Clear pending flag
|
||||
pendingCompanyRequests.current.delete(companyId);
|
||||
// Clear loading state
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: false }));
|
||||
}
|
||||
}, [companyLinesCache]);
|
||||
}
|
||||
}, [companyLinesCache, data]);
|
||||
|
||||
// Function to fetch sublines for a specific line - memoized
|
||||
const fetchSublines = useCallback(async (rowIndex: string | number, lineId: string) => {
|
||||
const fetchSublines = useCallback(async (rowIndex: string | number | undefined | null, lineId: string) => {
|
||||
try {
|
||||
// Only fetch if we have a valid line ID
|
||||
if (!lineId) return;
|
||||
|
||||
console.log(`Fetching sublines for row ${rowIndex}, line ${lineId}`);
|
||||
const logRowKey = (rowIndex !== undefined && rowIndex !== null) ? rowIndex : 'all-matching-rows';
|
||||
console.log(`Fetching sublines for row ${logRowKey}, line ${lineId}`);
|
||||
|
||||
// Check if we already have this line's sublines in the cache
|
||||
if (lineSublineCache[lineId]) {
|
||||
console.log(`Using cached sublines for line ${lineId}`);
|
||||
// Use cached data
|
||||
setRowSublines(prev => ({ ...prev, [rowIndex]: lineSublineCache[lineId] }));
|
||||
return lineSublineCache[lineId];
|
||||
const cached = lineSublineCache[lineId];
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setRowSublines(prev => ({ ...prev, [rowIndex]: cached }));
|
||||
}
|
||||
// Also update all rows with this line
|
||||
const updates: Record<string, any[]> = {};
|
||||
data.forEach((row, idx) => {
|
||||
if (row.line && String(row.line) === String(lineId)) {
|
||||
const key = (row.__index !== undefined && row.__index !== null) ? row.__index : idx;
|
||||
updates[key] = cached;
|
||||
}
|
||||
});
|
||||
if (Object.keys(updates).length > 0) {
|
||||
setRowSublines(prev => ({ ...prev, ...updates }));
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
// If a request for this line is already in flight, skip duplicate fetch
|
||||
if (pendingLineRequests.current.has(lineId)) {
|
||||
console.log(`Skipping fetch for line ${lineId} - request already pending`);
|
||||
return;
|
||||
}
|
||||
pendingLineRequests.current.add(lineId);
|
||||
|
||||
// Set loading state for this row
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: true }));
|
||||
}
|
||||
|
||||
// Fetch sublines from API
|
||||
const sublinesUrl = `/api/import/sublines/${lineId}`;
|
||||
@@ -109,8 +182,22 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
// Store in line cache
|
||||
setLineSublineCache(prev => ({ ...prev, [lineId]: formattedSublines }));
|
||||
|
||||
// Store for this specific row
|
||||
// Update specific row if provided
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setRowSublines(prev => ({ ...prev, [rowIndex]: formattedSublines }));
|
||||
}
|
||||
|
||||
// Also update all rows with this line
|
||||
const updates: Record<string, any[]> = {};
|
||||
data.forEach((row, idx) => {
|
||||
if (row.line && String(row.line) === String(lineId)) {
|
||||
const key = (row.__index !== undefined && row.__index !== null) ? row.__index : idx;
|
||||
updates[key] = formattedSublines;
|
||||
}
|
||||
});
|
||||
if (Object.keys(updates).length > 0) {
|
||||
setRowSublines(prev => ({ ...prev, ...updates }));
|
||||
}
|
||||
|
||||
return formattedSublines;
|
||||
} catch (error) {
|
||||
@@ -120,14 +207,20 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
|
||||
|
||||
// Store empty array for this specific row
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setRowSublines(prev => ({ ...prev, [rowIndex]: [] }));
|
||||
}
|
||||
|
||||
return [];
|
||||
} finally {
|
||||
// Clear pending flag
|
||||
pendingLineRequests.current.delete(lineId);
|
||||
// Clear loading state
|
||||
if (rowIndex !== undefined && rowIndex !== null) {
|
||||
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: false }));
|
||||
}
|
||||
}, [lineSublineCache]);
|
||||
}
|
||||
}, [lineSublineCache, data]);
|
||||
|
||||
// When data changes, fetch product lines and sublines for rows that have company/line values
|
||||
useEffect(() => {
|
||||
@@ -198,6 +291,11 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
|
||||
// Process companies that need product lines
|
||||
companiesNeeded.forEach((rowIds, companyId) => {
|
||||
// If this company is already being fetched, skip creating another request
|
||||
if (pendingCompanyRequests.current.has(companyId)) {
|
||||
console.log(`Skipping batch fetch for company ${companyId} - request already pending`);
|
||||
return;
|
||||
}
|
||||
// Skip if already in cache
|
||||
if (companyLinesCache[companyId]) {
|
||||
console.log(`Using cached product lines for company ${companyId}`);
|
||||
@@ -218,6 +316,8 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
|
||||
// Create fetch promise
|
||||
const fetchPromise = (async () => {
|
||||
// Mark this company as pending
|
||||
pendingCompanyRequests.current.add(companyId);
|
||||
// Safety timeout to ensure loading state is cleared after 10 seconds
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log(`Safety timeout triggered for company ${companyId}`);
|
||||
@@ -253,13 +353,19 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
const productLines = response.data;
|
||||
console.log(`Received ${productLines.length} product lines for company ${companyId}`);
|
||||
|
||||
// Format the data for dropdown display (consistent with single-row fetch)
|
||||
const formattedLines = productLines.map((line: any) => ({
|
||||
label: line.name || line.label || String(line.value || line.id),
|
||||
value: String(line.value || line.id)
|
||||
}));
|
||||
|
||||
// Store in company cache
|
||||
setCompanyLinesCache(prev => ({ ...prev, [companyId]: productLines }));
|
||||
setCompanyLinesCache(prev => ({ ...prev, [companyId]: formattedLines }));
|
||||
|
||||
// Update all rows with this company
|
||||
const updates: Record<string, any[]> = {};
|
||||
rowIds.forEach(rowId => {
|
||||
updates[rowId] = productLines;
|
||||
updates[rowId] = formattedLines;
|
||||
});
|
||||
setRowProductLines(prev => ({ ...prev, ...updates }));
|
||||
} catch (error) {
|
||||
@@ -278,6 +384,8 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
// Show error toast
|
||||
toast.error(`Failed to load product lines for company ${companyId}`);
|
||||
} finally {
|
||||
// Clear pending flag for company
|
||||
pendingCompanyRequests.current.delete(companyId);
|
||||
// Clear the safety timeout
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
@@ -295,6 +403,11 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
|
||||
// Process lines that need sublines
|
||||
linesNeeded.forEach((rowIds, lineId) => {
|
||||
// If this line is already being fetched, skip creating another request
|
||||
if (pendingLineRequests.current.has(lineId)) {
|
||||
console.log(`Skipping batch fetch for line ${lineId} - request already pending`);
|
||||
return;
|
||||
}
|
||||
// Skip if already in cache
|
||||
if (lineSublineCache[lineId]) {
|
||||
console.log(`Using cached sublines for line ${lineId}`);
|
||||
@@ -315,6 +428,8 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
|
||||
// Create fetch promise
|
||||
const fetchPromise = (async () => {
|
||||
// Mark this line as pending
|
||||
pendingLineRequests.current.add(lineId);
|
||||
// Safety timeout to ensure loading state is cleared after 10 seconds
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log(`Safety timeout triggered for line ${lineId}`);
|
||||
@@ -350,13 +465,19 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
const sublines = response.data;
|
||||
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
|
||||
|
||||
// Format the data for dropdown display (consistent with single-row fetch)
|
||||
const formattedSublines = sublines.map((subline: any) => ({
|
||||
label: subline.name || subline.label || String(subline.value || subline.id),
|
||||
value: String(subline.value || subline.id)
|
||||
}));
|
||||
|
||||
// Store in line cache
|
||||
setLineSublineCache(prev => ({ ...prev, [lineId]: sublines }));
|
||||
setLineSublineCache(prev => ({ ...prev, [lineId]: formattedSublines }));
|
||||
|
||||
// Update all rows with this line
|
||||
const updates: Record<string, any[]> = {};
|
||||
rowIds.forEach(rowId => {
|
||||
updates[rowId] = sublines;
|
||||
updates[rowId] = formattedSublines;
|
||||
});
|
||||
setRowSublines(prev => ({ ...prev, ...updates }));
|
||||
} catch (error) {
|
||||
@@ -375,6 +496,8 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||
// Show error toast
|
||||
toast.error(`Failed to load sublines for line ${lineId}`);
|
||||
} finally {
|
||||
// Clear pending flag for line
|
||||
pendingLineRequests.current.delete(lineId);
|
||||
// Clear the safety timeout
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
|
||||
@@ -35,9 +35,14 @@ export const useValidationState = <T extends string>({
|
||||
// Core data state
|
||||
const [data, setData] = useState<RowData<T>[]>(() => {
|
||||
// Clean price fields in initial data before setting state
|
||||
return initialData.map((row) => {
|
||||
return initialData.map((row, index) => {
|
||||
const updatedRow = { ...row } as Record<string, any>;
|
||||
|
||||
// Ensure each row has a stable __index key for downstream lookups
|
||||
if (updatedRow.__index === undefined || updatedRow.__index === null || updatedRow.__index === '') {
|
||||
updatedRow.__index = String(index);
|
||||
}
|
||||
|
||||
// Clean price fields using utility
|
||||
if (updatedRow.msrp !== undefined) {
|
||||
updatedRow.msrp = cleanPriceField(updatedRow.msrp);
|
||||
|
||||
Reference in New Issue
Block a user