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 { Textarea } from '@/components/ui/textarea';
|
||||
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { format } from 'date-fns';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
@@ -118,7 +116,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
product_type: '',
|
||||
});
|
||||
const [fieldOptions, setFieldOptions] = useState<FieldOptions | null>(null);
|
||||
const [productLines, setProductLines] = useState<FieldOption[]>([]);
|
||||
const [, setProductLines] = useState<FieldOption[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const productsPerPage = 500;
|
||||
@@ -528,9 +526,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// Change page
|
||||
|
||||
@@ -846,13 +846,45 @@ function useTemplates<T extends string>(
|
||||
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
|
||||
const fetchTemplates = useCallback(async () => {
|
||||
try {
|
||||
console.log('Fetching 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')
|
||||
|
||||
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) {
|
||||
console.error('Error fetching templates:', error)
|
||||
toast({
|
||||
@@ -883,108 +915,155 @@ function useTemplates<T extends string>(
|
||||
const applyTemplate = useCallback(async (templateId: string, rowIndices?: number[]) => {
|
||||
if (!templateId) return
|
||||
|
||||
const template = templates?.find(t => t.id.toString() === templateId)
|
||||
if (!template) return
|
||||
try {
|
||||
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>[]) => {
|
||||
const newData = [...prevData]
|
||||
const indicesToUpdate = rowIndices || newData.map((_, i) => i)
|
||||
setData((prevData: RowData<T>[]) => {
|
||||
try {
|
||||
const newData = [...prevData]
|
||||
const indicesToUpdate = rowIndices || newData.map((_, i) => i)
|
||||
|
||||
indicesToUpdate.forEach(index => {
|
||||
const row = newData[index]
|
||||
if (!row) return
|
||||
indicesToUpdate.forEach(index => {
|
||||
const row = newData[index]
|
||||
if (!row) return
|
||||
|
||||
// Apply all template fields except id, company, product_type, created_at, and updated_at
|
||||
Object.entries(template).forEach(([key, value]) => {
|
||||
if (!['id', 'company', 'product_type', 'created_at', 'updated_at'].includes(key)) {
|
||||
// Handle numeric values that might be stored as strings
|
||||
if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value)) {
|
||||
// If it's a price field, add the dollar sign
|
||||
if (['msrp', 'cost_each'].includes(key)) {
|
||||
row[key as keyof typeof row] = `$${value}` as any;
|
||||
} else {
|
||||
row[key as keyof typeof row] = value as any;
|
||||
}
|
||||
}
|
||||
// Special handling for categories field
|
||||
else if (key === 'categories') {
|
||||
console.log('Applying categories from template:', {
|
||||
key,
|
||||
value,
|
||||
type: typeof value,
|
||||
isArray: Array.isArray(value)
|
||||
});
|
||||
// Apply all template fields except id, company, product_type, created_at, and updated_at
|
||||
Object.entries(template).forEach(([key, value]) => {
|
||||
if (!['id', 'company', 'product_type', 'created_at', 'updated_at'].includes(key)) {
|
||||
// Handle numeric values that might be stored as strings
|
||||
if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value)) {
|
||||
// If it's a price field, add the dollar sign
|
||||
if (['msrp', 'cost_each'].includes(key)) {
|
||||
row[key as keyof typeof row] = `$${value}` as any;
|
||||
} else {
|
||||
row[key as keyof typeof row] = value as any;
|
||||
}
|
||||
}
|
||||
// Special handling for categories field
|
||||
else if (key === 'categories') {
|
||||
console.log('Applying categories from template:', {
|
||||
key,
|
||||
value,
|
||||
type: typeof value,
|
||||
isArray: Array.isArray(value)
|
||||
});
|
||||
|
||||
// If categories is an array, use it directly
|
||||
if (Array.isArray(value)) {
|
||||
row[key as keyof typeof row] = value as any;
|
||||
console.log('Categories is array, using directly:', value);
|
||||
}
|
||||
// If categories is a string (possibly a PostgreSQL array representation), 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;
|
||||
console.log('Categories parsed from JSON string:', parsed);
|
||||
// If categories is an array, use it directly
|
||||
if (Array.isArray(value)) {
|
||||
row[key as keyof typeof row] = value as any;
|
||||
console.log('Categories is array, using directly:', value);
|
||||
}
|
||||
// Handle PostgreSQL array format like {value1,value2}
|
||||
else if (value.startsWith('{') && value.endsWith('}')) {
|
||||
const parsed = value.substring(1, value.length - 1).split(',');
|
||||
row[key as keyof typeof row] = parsed as any;
|
||||
console.log('Categories parsed from PostgreSQL array:', parsed);
|
||||
// If categories is a string (possibly a PostgreSQL array representation), 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;
|
||||
console.log('Categories parsed from JSON string:', parsed);
|
||||
}
|
||||
// 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;
|
||||
console.log('Categories parsed from PostgreSQL array:', parsed);
|
||||
}
|
||||
// If it's a single value, wrap it in an array
|
||||
else {
|
||||
row[key as keyof typeof row] = [value] as any;
|
||||
console.log('Categories is single value, wrapping in array:', [value]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing categories:', error);
|
||||
// If parsing fails, use as-is
|
||||
row[key as keyof typeof row] = value as any;
|
||||
}
|
||||
}
|
||||
// If it's a comma-separated string
|
||||
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
|
||||
// For any other type, use as-is
|
||||
else {
|
||||
row[key as keyof typeof row] = [] as any;
|
||||
console.log('Categories is empty string, using empty array');
|
||||
row[key as keyof typeof row] = value as any;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing categories:', e);
|
||||
row[key as keyof typeof row] = [] 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;
|
||||
}
|
||||
}
|
||||
// Default to empty array for any other case
|
||||
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 {
|
||||
row[key as keyof typeof row] = value as any;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Update the template reference
|
||||
row.__template = templateId;
|
||||
// Update the template reference
|
||||
row.__template = templateId;
|
||||
})
|
||||
|
||||
return newData
|
||||
} catch (error) {
|
||||
console.error('Error applying template:', error);
|
||||
return prevData;
|
||||
}
|
||||
})
|
||||
|
||||
return newData
|
||||
})
|
||||
|
||||
toast({
|
||||
title: "Template Applied",
|
||||
description: `Applied template to ${rowIndices?.length || data.length} row(s)`,
|
||||
})
|
||||
toast({
|
||||
title: "Template Applied",
|
||||
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])
|
||||
|
||||
const saveAsTemplate = useCallback(async () => {
|
||||
@@ -1089,6 +1168,36 @@ function useTemplates<T extends string>(
|
||||
}
|
||||
}, [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 {
|
||||
templates,
|
||||
selectedTemplateId: state.selectedTemplateId,
|
||||
@@ -1101,10 +1210,370 @@ function useTemplates<T extends string>(
|
||||
newTemplateType: state.newTemplateType,
|
||||
applyTemplate,
|
||||
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(({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -1426,6 +1895,7 @@ export const ValidationStep = <T extends string>({
|
||||
saveAsTemplate,
|
||||
setNewTemplateName,
|
||||
setNewTemplateType,
|
||||
getTemplateDisplayText,
|
||||
} = useTemplates(data, setData, useToast, rowSelection)
|
||||
|
||||
// Memoize filtered data to prevent recalculation on every render
|
||||
@@ -1527,31 +1997,52 @@ export const ValidationStep = <T extends string>({
|
||||
{
|
||||
id: "template",
|
||||
header: "Template",
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
value={row.original.__template || ""}
|
||||
onValueChange={(value) => {
|
||||
const newData = [...data];
|
||||
const index = newData.findIndex(r => r.__index === row.original.__index);
|
||||
if (index !== -1) {
|
||||
newData[index] = { ...newData[index], __template: value };
|
||||
setData(newData);
|
||||
applyTemplate(value, [index]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select template" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates?.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id.toString()}>
|
||||
{template.company} - {template.product_type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
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 || ""}
|
||||
onValueChange={(value) => {
|
||||
try {
|
||||
const newData = [...data];
|
||||
const index = newData.findIndex(r => r.__index === row.original.__index);
|
||||
if (index !== -1) {
|
||||
newData[index] = { ...newData[index], __template: value };
|
||||
setData(newData);
|
||||
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}
|
||||
defaultBrand={globalSelections?.company}
|
||||
/>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error rendering template cell:", error);
|
||||
return (
|
||||
<Button variant="outline" className="w-full text-destructive">
|
||||
Error loading templates
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
size: 200,
|
||||
},
|
||||
...(Array.from(fields as ReadonlyFields<T>).map((field): ColumnDef<Data<T> & ExtendedMeta> => ({
|
||||
@@ -1589,7 +2080,7 @@ export const ValidationStep = <T extends string>({
|
||||
})))
|
||||
]
|
||||
return baseColumns
|
||||
}, [fields, updateRows, data, copyValueDown, templates, applyTemplate, productLines, sublines])
|
||||
}, [fields, updateRows, data, copyValueDown, templates, applyTemplate, productLines, sublines, getTemplateDisplayText])
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
@@ -2839,25 +3330,37 @@ export const ValidationStep = <T extends string>({
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedTemplateId || ""}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTemplateId(value);
|
||||
const selectedRows = Object.keys(rowSelection).map(Number);
|
||||
applyTemplate(value, selectedRows);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-8 bg-card text-xs font-semibold">
|
||||
<SelectValue placeholder="Apply template" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates?.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id.toString()}>
|
||||
{template.company} - {template.product_type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center">
|
||||
{/* Wrap in a fragment instead of an IIFE */}
|
||||
{templates && templates.length > 0 ? (
|
||||
<SearchableTemplateSelect
|
||||
templates={templates}
|
||||
value={selectedTemplateId || ""}
|
||||
onValueChange={(value) => {
|
||||
try {
|
||||
setSelectedTemplateId(value);
|
||||
const selectedRows = Object.keys(rowSelection).map(Number);
|
||||
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}
|
||||
placeholder="Select template"
|
||||
triggerClassName="w-[200px]"
|
||||
defaultBrand={globalSelections?.company}
|
||||
/>
|
||||
) : (
|
||||
<Button variant="outline" className="w-full justify-between" disabled>
|
||||
Loading templates...
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Object.keys(rowSelection).length === 1 && (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user