Improve template search in validate step
This commit is contained in:
@@ -6,15 +6,13 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Loader2, Search, ChevronsUpDown, Check, ChevronLeft, ChevronRight, MoreHorizontal, X, ChevronUp, ChevronDown } from 'lucide-react';
|
import { Loader2, Search, ChevronsUpDown, Check, X, ChevronUp, ChevronDown } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
@@ -118,7 +116,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
product_type: '',
|
product_type: '',
|
||||||
});
|
});
|
||||||
const [fieldOptions, setFieldOptions] = useState<FieldOptions | null>(null);
|
const [fieldOptions, setFieldOptions] = useState<FieldOptions | null>(null);
|
||||||
const [productLines, setProductLines] = useState<FieldOption[]>([]);
|
const [, setProductLines] = useState<FieldOption[]>([]);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const productsPerPage = 500;
|
const productsPerPage = 500;
|
||||||
@@ -528,9 +526,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get current products for pagination
|
// Get current products for pagination
|
||||||
const indexOfLastProduct = currentPage * productsPerPage;
|
|
||||||
const indexOfFirstProduct = indexOfLastProduct - productsPerPage;
|
|
||||||
const currentProducts = searchResults.slice(indexOfFirstProduct, indexOfLastProduct);
|
|
||||||
const totalPages = Math.ceil(searchResults.length / productsPerPage);
|
const totalPages = Math.ceil(searchResults.length / productsPerPage);
|
||||||
|
|
||||||
// Change page
|
// Change page
|
||||||
|
|||||||
@@ -846,13 +846,45 @@ function useTemplates<T extends string>(
|
|||||||
newTemplateType: "",
|
newTemplateType: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Fetch field options to get company names
|
||||||
|
const { data: fieldOptions } = useQuery({
|
||||||
|
queryKey: ["field-options"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`${config.apiUrl}/import/field-options`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch field options");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
staleTime: 600000, // 10 minutes
|
||||||
|
});
|
||||||
|
|
||||||
// Function to fetch templates
|
// Function to fetch templates
|
||||||
const fetchTemplates = useCallback(async () => {
|
const fetchTemplates = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('Fetching templates...');
|
||||||
const response = await fetch(`${config.apiUrl}/templates`)
|
const response = await fetch(`${config.apiUrl}/templates`)
|
||||||
|
console.log('Templates response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch templates')
|
if (!response.ok) throw new Error('Failed to fetch templates')
|
||||||
|
|
||||||
const templateData = await response.json()
|
const templateData = await response.json()
|
||||||
setTemplates(templateData)
|
console.log('Templates fetched successfully:', templateData);
|
||||||
|
console.log('First template:', templateData[0]);
|
||||||
|
|
||||||
|
// Validate template data
|
||||||
|
const validTemplates = templateData.filter((t: any) =>
|
||||||
|
t && typeof t === 'object' && t.id && t.company && t.product_type
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validTemplates.length !== templateData.length) {
|
||||||
|
console.warn('Some templates were filtered out due to invalid data', {
|
||||||
|
original: templateData.length,
|
||||||
|
valid: validTemplates.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplates(validTemplates)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching templates:', error)
|
console.error('Error fetching templates:', error)
|
||||||
toast({
|
toast({
|
||||||
@@ -883,10 +915,20 @@ function useTemplates<T extends string>(
|
|||||||
const applyTemplate = useCallback(async (templateId: string, rowIndices?: number[]) => {
|
const applyTemplate = useCallback(async (templateId: string, rowIndices?: number[]) => {
|
||||||
if (!templateId) return
|
if (!templateId) return
|
||||||
|
|
||||||
const template = templates?.find(t => t.id.toString() === templateId)
|
try {
|
||||||
if (!template) return
|
const template = templates?.find(t => t && t.id && t.id.toString() === templateId)
|
||||||
|
if (!template) {
|
||||||
|
console.error(`Template with ID ${templateId} not found`);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Template not found`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setData((prevData: RowData<T>[]) => {
|
setData((prevData: RowData<T>[]) => {
|
||||||
|
try {
|
||||||
const newData = [...prevData]
|
const newData = [...prevData]
|
||||||
const indicesToUpdate = rowIndices || newData.map((_, i) => i)
|
const indicesToUpdate = rowIndices || newData.map((_, i) => i)
|
||||||
|
|
||||||
@@ -929,62 +971,99 @@ function useTemplates<T extends string>(
|
|||||||
row[key as keyof typeof row] = parsed as any;
|
row[key as keyof typeof row] = parsed as any;
|
||||||
console.log('Categories parsed from JSON string:', parsed);
|
console.log('Categories parsed from JSON string:', parsed);
|
||||||
}
|
}
|
||||||
// Handle PostgreSQL array format like {value1,value2}
|
// Otherwise, it might be a PostgreSQL array format like {val1,val2}
|
||||||
else if (value.startsWith('{') && value.endsWith('}')) {
|
else if (value.startsWith('{') && value.endsWith('}')) {
|
||||||
const parsed = value.substring(1, value.length - 1).split(',');
|
const parsed = value.slice(1, -1).split(',');
|
||||||
row[key as keyof typeof row] = parsed as any;
|
row[key as keyof typeof row] = parsed as any;
|
||||||
console.log('Categories parsed from PostgreSQL array:', parsed);
|
console.log('Categories parsed from PostgreSQL array:', parsed);
|
||||||
}
|
}
|
||||||
// If it's a comma-separated string
|
// If it's a single value, wrap it in an array
|
||||||
else if (value.includes(',')) {
|
|
||||||
const parsed = value.split(',');
|
|
||||||
row[key as keyof typeof row] = parsed as any;
|
|
||||||
console.log('Categories parsed from comma-separated string:', parsed);
|
|
||||||
}
|
|
||||||
// If it's a single value
|
|
||||||
else if (value.trim()) {
|
|
||||||
const parsed = [value.trim()];
|
|
||||||
row[key as keyof typeof row] = parsed as any;
|
|
||||||
console.log('Categories parsed from single value:', parsed);
|
|
||||||
}
|
|
||||||
// Empty value
|
|
||||||
else {
|
else {
|
||||||
row[key as keyof typeof row] = [] as any;
|
row[key as keyof typeof row] = [value] as any;
|
||||||
console.log('Categories is empty string, using empty array');
|
console.log('Categories is single value, wrapping in array:', [value]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error('Error parsing categories:', e);
|
console.error('Error parsing categories:', error);
|
||||||
row[key as keyof typeof row] = [] as any;
|
// If parsing fails, use as-is
|
||||||
|
row[key as keyof typeof row] = value as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Default to empty array for any other case
|
// For any other type, use as-is
|
||||||
else {
|
|
||||||
row[key as keyof typeof row] = [] as any;
|
|
||||||
console.log('Categories is not array or string, using empty array');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle other array values
|
|
||||||
else if (Array.isArray(value)) {
|
|
||||||
row[key as keyof typeof row] = [...value] as any;
|
|
||||||
}
|
|
||||||
// Handle other values
|
|
||||||
else {
|
else {
|
||||||
row[key as keyof typeof row] = value as any;
|
row[key as keyof typeof row] = value as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
// Handle ship_restrictions field similarly to categories
|
||||||
|
else if (key === 'ship_restrictions') {
|
||||||
|
console.log('Applying ship_restrictions from template:', {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
type: typeof value,
|
||||||
|
isArray: Array.isArray(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// If ship_restrictions is an array, use it directly
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
row[key as keyof typeof row] = value as any;
|
||||||
|
}
|
||||||
|
// If ship_restrictions is a string, try to parse it
|
||||||
|
else if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
// Try to parse as JSON if it's a JSON string
|
||||||
|
if (value.startsWith('[') && value.endsWith(']')) {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
row[key as keyof typeof row] = parsed as any;
|
||||||
|
}
|
||||||
|
// Otherwise, it might be a PostgreSQL array format like {val1,val2}
|
||||||
|
else if (value.startsWith('{') && value.endsWith('}')) {
|
||||||
|
const parsed = value.slice(1, -1).split(',');
|
||||||
|
row[key as keyof typeof row] = parsed as any;
|
||||||
|
}
|
||||||
|
// If it's a single value, wrap it in an array
|
||||||
|
else {
|
||||||
|
row[key as keyof typeof row] = [value] as any;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing ship_restrictions:', error);
|
||||||
|
// If parsing fails, use as-is
|
||||||
|
row[key as keyof typeof row] = value as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For any other type, use as-is
|
||||||
|
else {
|
||||||
|
row[key as keyof typeof row] = value as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For all other fields, apply directly
|
||||||
|
else {
|
||||||
|
row[key as keyof typeof row] = value as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update the template reference
|
// Update the template reference
|
||||||
row.__template = templateId;
|
row.__template = templateId;
|
||||||
})
|
})
|
||||||
|
|
||||||
return newData
|
return newData
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying template:', error);
|
||||||
|
return prevData;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Template Applied",
|
title: "Template Applied",
|
||||||
description: `Applied template to ${rowIndices?.length || data.length} row(s)`,
|
description: `Applied template to ${rowIndices?.length || data.length} row(s)`,
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in applyTemplate:', error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to apply template",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [templates, setData, data.length, toast])
|
}, [templates, setData, data.length, toast])
|
||||||
|
|
||||||
const saveAsTemplate = useCallback(async () => {
|
const saveAsTemplate = useCallback(async () => {
|
||||||
@@ -1089,6 +1168,36 @@ function useTemplates<T extends string>(
|
|||||||
}
|
}
|
||||||
}, [state, data, rowSelection, toast, setTemplates, setState, setData]);
|
}, [state, data, rowSelection, toast, setTemplates, setState, setData]);
|
||||||
|
|
||||||
|
// Helper function to get company name from ID
|
||||||
|
const getCompanyName = useCallback((companyId: string) => {
|
||||||
|
if (!fieldOptions || !fieldOptions.companies) return companyId;
|
||||||
|
try {
|
||||||
|
const company = fieldOptions.companies.find((c: { value: string; label: string }) => c.value === companyId);
|
||||||
|
return company ? company.label : companyId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting company name:", error);
|
||||||
|
return companyId;
|
||||||
|
}
|
||||||
|
}, [fieldOptions]);
|
||||||
|
|
||||||
|
// Format template display text
|
||||||
|
const getTemplateDisplayText = useCallback((template: Template) => {
|
||||||
|
if (!template) {
|
||||||
|
console.error("Template is null or undefined in getTemplateDisplayText");
|
||||||
|
return "Unknown Template";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const companyId = template.company || "";
|
||||||
|
const productType = template.product_type || "Unknown Type";
|
||||||
|
const companyName = getCompanyName(companyId);
|
||||||
|
return `${companyName} - ${productType}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error formatting template display text:", error, template);
|
||||||
|
return "Error displaying template";
|
||||||
|
}
|
||||||
|
}, [getCompanyName]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
templates,
|
templates,
|
||||||
selectedTemplateId: state.selectedTemplateId,
|
selectedTemplateId: state.selectedTemplateId,
|
||||||
@@ -1101,10 +1210,370 @@ function useTemplates<T extends string>(
|
|||||||
newTemplateType: state.newTemplateType,
|
newTemplateType: state.newTemplateType,
|
||||||
applyTemplate,
|
applyTemplate,
|
||||||
saveAsTemplate,
|
saveAsTemplate,
|
||||||
|
getTemplateDisplayText,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this component before the ValidationStep component
|
// Add this component before the SaveTemplateDialog component
|
||||||
|
const SearchableTemplateSelect = memo(({
|
||||||
|
templates,
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
getTemplateDisplayText,
|
||||||
|
placeholder = "Select template",
|
||||||
|
className,
|
||||||
|
triggerClassName,
|
||||||
|
defaultBrand,
|
||||||
|
}: {
|
||||||
|
templates: Template[];
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
getTemplateDisplayText: (template: Template) => string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
triggerClassName?: string;
|
||||||
|
defaultBrand?: string;
|
||||||
|
}) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [selectedBrand, setSelectedBrand] = useState<string | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isTemplatesReady, setIsTemplatesReady] = useState(false);
|
||||||
|
|
||||||
|
// Set default brand when component mounts or defaultBrand changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultBrand) {
|
||||||
|
setSelectedBrand(defaultBrand);
|
||||||
|
}
|
||||||
|
}, [defaultBrand]);
|
||||||
|
|
||||||
|
// Handle wheel events for scrolling
|
||||||
|
const handleWheel = (e: React.WheelEvent) => {
|
||||||
|
const scrollArea = e.currentTarget;
|
||||||
|
scrollArea.scrollTop += e.deltaY;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log props for debugging
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('SearchableTemplateSelect props:', {
|
||||||
|
templatesCount: templates?.length || 0,
|
||||||
|
value,
|
||||||
|
hasGetTemplateDisplayText: !!getTemplateDisplayText,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
triggerClassName
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if templates are valid
|
||||||
|
if (templates && templates.length > 0) {
|
||||||
|
const firstTemplate = templates[0];
|
||||||
|
console.log('First template:', firstTemplate);
|
||||||
|
setIsTemplatesReady(true);
|
||||||
|
} else {
|
||||||
|
setIsTemplatesReady(false);
|
||||||
|
}
|
||||||
|
}, [templates, value, getTemplateDisplayText, placeholder, className, triggerClassName]);
|
||||||
|
|
||||||
|
// Extract unique brands from templates
|
||||||
|
const brands = useMemo(() => {
|
||||||
|
try {
|
||||||
|
if (!templates || templates.length === 0) return [];
|
||||||
|
|
||||||
|
const brandSet = new Set<string>();
|
||||||
|
const brandNames: {id: string, name: string}[] = [];
|
||||||
|
|
||||||
|
templates.forEach(template => {
|
||||||
|
if (!template || !template.company) return;
|
||||||
|
|
||||||
|
const companyId = template.company;
|
||||||
|
if (!brandSet.has(companyId)) {
|
||||||
|
brandSet.add(companyId);
|
||||||
|
|
||||||
|
// Try to get the company name from the template display text
|
||||||
|
try {
|
||||||
|
const displayText = getTemplateDisplayText(template);
|
||||||
|
const companyName = displayText.split(' - ')[0];
|
||||||
|
brandNames.push({ id: companyId, name: companyName || `Company ${companyId}` });
|
||||||
|
} catch (err) {
|
||||||
|
brandNames.push({ id: companyId, name: `Company ${companyId}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return brandNames.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error extracting brands:", err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [templates, getTemplateDisplayText]);
|
||||||
|
|
||||||
|
// Group templates by company for better organization
|
||||||
|
const groupedTemplates = useMemo(() => {
|
||||||
|
try {
|
||||||
|
if (!templates || templates.length === 0) return {};
|
||||||
|
|
||||||
|
const groups: Record<string, Template[]> = {};
|
||||||
|
|
||||||
|
templates.forEach(template => {
|
||||||
|
if (!template) return;
|
||||||
|
|
||||||
|
const companyId = template.company;
|
||||||
|
if (!groups[companyId]) {
|
||||||
|
groups[companyId] = [];
|
||||||
|
}
|
||||||
|
groups[companyId].push(template);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error grouping templates:", err);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}, [templates]);
|
||||||
|
|
||||||
|
// Filter templates based on selected brand and search term
|
||||||
|
const filteredTemplates = useMemo(() => {
|
||||||
|
try {
|
||||||
|
if (!templates || templates.length === 0) return [];
|
||||||
|
|
||||||
|
// First filter by brand if selected
|
||||||
|
let brandFiltered = templates;
|
||||||
|
if (selectedBrand) {
|
||||||
|
brandFiltered = templates.filter(t => t && t.company === selectedBrand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then filter by search term if provided
|
||||||
|
if (!searchTerm.trim()) return brandFiltered;
|
||||||
|
|
||||||
|
const lowerSearchTerm = searchTerm.toLowerCase();
|
||||||
|
return brandFiltered.filter(template => {
|
||||||
|
if (!template) return false;
|
||||||
|
try {
|
||||||
|
const displayText = getTemplateDisplayText(template);
|
||||||
|
const productType = template.product_type?.toLowerCase() || '';
|
||||||
|
|
||||||
|
// Search in both the display text and product type
|
||||||
|
return displayText.toLowerCase().includes(lowerSearchTerm) ||
|
||||||
|
productType.includes(lowerSearchTerm);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error filtering template:", error, template);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in filteredTemplates:", err);
|
||||||
|
setError("Error filtering templates");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [templates, selectedBrand, searchTerm, getTemplateDisplayText]);
|
||||||
|
|
||||||
|
// Get the display text for the selected template
|
||||||
|
const selectedTemplate = useMemo(() => {
|
||||||
|
try {
|
||||||
|
if (!templates || templates.length === 0 || !value) return null;
|
||||||
|
return templates.find(t => t && t.id && t.id.toString() === value);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error finding selected template:", err);
|
||||||
|
setError("Error finding selected template");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [templates, value]);
|
||||||
|
|
||||||
|
// Handle errors gracefully
|
||||||
|
const getDisplayText = useCallback((template: Template | null) => {
|
||||||
|
try {
|
||||||
|
if (!template) return placeholder;
|
||||||
|
return getTemplateDisplayText(template);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error getting template display text:", err);
|
||||||
|
setError("Error displaying template");
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
}, [getTemplateDisplayText, placeholder]);
|
||||||
|
|
||||||
|
// Reset filters
|
||||||
|
const resetFilters = useCallback(() => {
|
||||||
|
setSearchTerm("");
|
||||||
|
setSelectedBrand(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle errors in the component
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn("w-full justify-between text-destructive", triggerClassName)}
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
>
|
||||||
|
Error: {error}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe render function for CommandItem
|
||||||
|
const renderCommandItem = useCallback((template: Template) => {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={template.id}
|
||||||
|
value={template.id.toString()}
|
||||||
|
onSelect={(currentValue) => {
|
||||||
|
try {
|
||||||
|
onValueChange(currentValue);
|
||||||
|
setOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
setSelectedBrand(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in onSelect:", err);
|
||||||
|
setError("Error selecting template");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-start gap-2 py-2"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-1 h-4 w-4 mt-0.5",
|
||||||
|
value === template.id.toString() ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{getDisplayText(template)}</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error rendering CommandItem:", err, template);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [getDisplayText, onValueChange, value]);
|
||||||
|
|
||||||
|
// Ensure we have templates before rendering the dropdown
|
||||||
|
if (!templates || templates.length === 0) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn("w-full justify-between", triggerClassName)}
|
||||||
|
>
|
||||||
|
No templates available
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={(newOpen) => {
|
||||||
|
try {
|
||||||
|
// Only allow opening if templates are ready
|
||||||
|
if (newOpen && !isTemplatesReady) {
|
||||||
|
console.warn("Prevented opening popover because templates are not ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(newOpen);
|
||||||
|
if (!newOpen) {
|
||||||
|
// Reset filters when closing
|
||||||
|
resetFilters();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in onOpenChange:", err);
|
||||||
|
setError("Error opening dropdown");
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between",
|
||||||
|
triggerClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value && selectedTemplate
|
||||||
|
? getDisplayText(selectedTemplate)
|
||||||
|
: placeholder}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className={cn("w-[350px] p-0", className)}>
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
{/* Filter controls */}
|
||||||
|
<div className="flex flex-col border-b">
|
||||||
|
{/* Brand filter */}
|
||||||
|
<div className="flex items-center px-3 py-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Select
|
||||||
|
value={selectedBrand || "all"}
|
||||||
|
onValueChange={(value) => setSelectedBrand(value === "all" ? null : value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="All brands" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All brands</SelectItem>
|
||||||
|
{brands.map(brand => (
|
||||||
|
<SelectItem key={brand.id} value={brand.id}>
|
||||||
|
{brand.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search input */}
|
||||||
|
<div className="flex items-center px-3 pb-2">
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search by product type..."
|
||||||
|
value={searchTerm}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
try {
|
||||||
|
setSearchTerm(value);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in onValueChange:", err);
|
||||||
|
setError("Error searching templates");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-8 flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
<CommandEmpty>
|
||||||
|
<div className="py-6 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">No templates found.</p>
|
||||||
|
</div>
|
||||||
|
</CommandEmpty>
|
||||||
|
|
||||||
|
<CommandList>
|
||||||
|
<ScrollArea className="max-h-[200px] overflow-y-auto" onWheel={handleWheel}>
|
||||||
|
{!selectedBrand && !searchTerm ? (
|
||||||
|
// When no filters are applied, show templates grouped by company
|
||||||
|
Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => {
|
||||||
|
// Get company name from the first template
|
||||||
|
const brand = brands.find(b => b.id === companyId);
|
||||||
|
const companyName = brand ? brand.name : `Company ${companyId}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandGroup key={companyId} heading={companyName}>
|
||||||
|
{companyTemplates.map(template => renderCommandItem(template))}
|
||||||
|
</CommandGroup>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
// When filters are applied, show filtered results
|
||||||
|
<CommandGroup>
|
||||||
|
{filteredTemplates.map(template => template ? renderCommandItem(template) : null)}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const SaveTemplateDialog = memo(({
|
const SaveTemplateDialog = memo(({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -1426,6 +1895,7 @@ export const ValidationStep = <T extends string>({
|
|||||||
saveAsTemplate,
|
saveAsTemplate,
|
||||||
setNewTemplateName,
|
setNewTemplateName,
|
||||||
setNewTemplateType,
|
setNewTemplateType,
|
||||||
|
getTemplateDisplayText,
|
||||||
} = useTemplates(data, setData, useToast, rowSelection)
|
} = useTemplates(data, setData, useToast, rowSelection)
|
||||||
|
|
||||||
// Memoize filtered data to prevent recalculation on every render
|
// Memoize filtered data to prevent recalculation on every render
|
||||||
@@ -1527,10 +1997,23 @@ export const ValidationStep = <T extends string>({
|
|||||||
{
|
{
|
||||||
id: "template",
|
id: "template",
|
||||||
header: "Template",
|
header: "Template",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Select
|
try {
|
||||||
|
// Only render the component if templates are available
|
||||||
|
if (!templates || templates.length === 0) {
|
||||||
|
return (
|
||||||
|
<Button variant="outline" className="w-full justify-between" disabled>
|
||||||
|
Loading templates...
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchableTemplateSelect
|
||||||
|
templates={templates}
|
||||||
value={row.original.__template || ""}
|
value={row.original.__template || ""}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
try {
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
const index = newData.findIndex(r => r.__index === row.original.__index);
|
const index = newData.findIndex(r => r.__index === row.original.__index);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -1538,20 +2021,28 @@ export const ValidationStep = <T extends string>({
|
|||||||
setData(newData);
|
setData(newData);
|
||||||
applyTemplate(value, [index]);
|
applyTemplate(value, [index]);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error applying template in cell:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to apply template to row",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
getTemplateDisplayText={getTemplateDisplayText}
|
||||||
<SelectTrigger className="w-full">
|
defaultBrand={globalSelections?.company}
|
||||||
<SelectValue placeholder="Select template" />
|
/>
|
||||||
</SelectTrigger>
|
);
|
||||||
<SelectContent>
|
} catch (error) {
|
||||||
{templates?.map((template) => (
|
console.error("Error rendering template cell:", error);
|
||||||
<SelectItem key={template.id} value={template.id.toString()}>
|
return (
|
||||||
{template.company} - {template.product_type}
|
<Button variant="outline" className="w-full text-destructive">
|
||||||
</SelectItem>
|
Error loading templates
|
||||||
))}
|
</Button>
|
||||||
</SelectContent>
|
);
|
||||||
</Select>
|
}
|
||||||
),
|
},
|
||||||
size: 200,
|
size: 200,
|
||||||
},
|
},
|
||||||
...(Array.from(fields as ReadonlyFields<T>).map((field): ColumnDef<Data<T> & ExtendedMeta> => ({
|
...(Array.from(fields as ReadonlyFields<T>).map((field): ColumnDef<Data<T> & ExtendedMeta> => ({
|
||||||
@@ -1589,7 +2080,7 @@ export const ValidationStep = <T extends string>({
|
|||||||
})))
|
})))
|
||||||
]
|
]
|
||||||
return baseColumns
|
return baseColumns
|
||||||
}, [fields, updateRows, data, copyValueDown, templates, applyTemplate, productLines, sublines])
|
}, [fields, updateRows, data, copyValueDown, templates, applyTemplate, productLines, sublines, getTemplateDisplayText])
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredData,
|
data: filteredData,
|
||||||
@@ -2839,25 +3330,37 @@ export const ValidationStep = <T extends string>({
|
|||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<div className="flex items-center">
|
||||||
|
{/* Wrap in a fragment instead of an IIFE */}
|
||||||
|
{templates && templates.length > 0 ? (
|
||||||
|
<SearchableTemplateSelect
|
||||||
|
templates={templates}
|
||||||
value={selectedTemplateId || ""}
|
value={selectedTemplateId || ""}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
try {
|
||||||
setSelectedTemplateId(value);
|
setSelectedTemplateId(value);
|
||||||
const selectedRows = Object.keys(rowSelection).map(Number);
|
const selectedRows = Object.keys(rowSelection).map(Number);
|
||||||
applyTemplate(value, selectedRows);
|
applyTemplate(value, selectedRows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error applying template to selection:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to apply template to selected rows",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
getTemplateDisplayText={getTemplateDisplayText}
|
||||||
<SelectTrigger className="w-[180px] h-8 bg-card text-xs font-semibold">
|
placeholder="Select template"
|
||||||
<SelectValue placeholder="Apply template" />
|
triggerClassName="w-[200px]"
|
||||||
</SelectTrigger>
|
defaultBrand={globalSelections?.company}
|
||||||
<SelectContent>
|
/>
|
||||||
{templates?.map((template) => (
|
) : (
|
||||||
<SelectItem key={template.id} value={template.id.toString()}>
|
<Button variant="outline" className="w-full justify-between" disabled>
|
||||||
{template.company} - {template.product_type}
|
Loading templates...
|
||||||
</SelectItem>
|
</Button>
|
||||||
))}
|
)}
|
||||||
</SelectContent>
|
</div>
|
||||||
</Select>
|
|
||||||
|
|
||||||
{Object.keys(rowSelection).length === 1 && (
|
{Object.keys(rowSelection).length === 1 && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user