From a19a8ba41257c9e2a418a3ef82d5ee308d739c05 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Feb 2025 01:16:01 -0500 Subject: [PATCH] Move image from URL option from validate step to add images step --- .../steps/ImageUploadStep/ImageUploadStep.tsx | 322 ++++++++++++++---- inventory/src/pages/Import.tsx | 7 - 2 files changed, 256 insertions(+), 73 deletions(-) diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ImageUploadStep/ImageUploadStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ImageUploadStep/ImageUploadStep.tsx index 27338fe..54dbee2 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ImageUploadStep/ImageUploadStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ImageUploadStep/ImageUploadStep.tsx @@ -1,7 +1,7 @@ import { useCallback, useState, useRef, useEffect } from "react"; import { useRsi } from "../../hooks/useRsi"; import { Button } from "@/components/ui/button"; -import { Loader2, Upload, Trash2, AlertCircle, GripVertical, Maximize2, X } from "lucide-react"; +import { Loader2, Upload, Trash2, AlertCircle, GripVertical, Maximize2, X, Link2 } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { toast } from "sonner"; import { Input } from "@/components/ui/input"; @@ -37,6 +37,11 @@ import { DialogContent, DialogTrigger, } from "@/components/ui/dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; type Props = { data: any[]; @@ -70,7 +75,6 @@ export const ImageUploadStep = ({ }: Props) => { const { translations } = useRsi(); const [isSubmitting, setIsSubmitting] = useState(false); - const [productImages, setProductImages] = useState([]); const fileInputRefs = useRef<{ [key: number]: HTMLInputElement | null }>({}); const [unassignedImages, setUnassignedImages] = useState([]); const [processingBulk, setProcessingBulk] = useState(false); @@ -78,6 +82,49 @@ export const ImageUploadStep = ({ const [activeId, setActiveId] = useState(null); const [activeImage, setActiveImage] = useState(null); + // Add state for URL input + const [urlInputs, setUrlInputs] = useState<{ [key: number]: string }>({}); + const [processingUrls, setProcessingUrls] = useState<{ [key: number]: boolean }>({}); + + // Initialize product images from data + const [productImages, setProductImages] = useState(() => { + // Convert existing product_images to ProductImageSortable objects + const initialImages: ProductImageSortable[] = []; + + data.forEach((product, productIndex) => { + if (product.product_images) { + let imageUrls: string[] = []; + + // Handle different formats of product_images + if (typeof product.product_images === 'string') { + // Split by comma if it's a string + imageUrls = product.product_images.split(',').filter(Boolean); + } else if (Array.isArray(product.product_images)) { + // Use the array directly + imageUrls = product.product_images.filter(Boolean); + } else if (product.product_images) { + // Handle case where it might be a single value + imageUrls = [String(product.product_images)]; + } + + // Create ProductImageSortable objects for each URL + imageUrls.forEach((url, i) => { + if (url && url.trim()) { + initialImages.push({ + id: `image-${productIndex}-initial-${i}`, + productIndex, + imageUrl: url.trim(), + loading: false, + fileName: `Image ${i + 1}` + }); + } + }); + } + }); + + return initialImages; + }); + // Set up sensors for drag and drop with enhanced configuration const sensors = useSensors( useSensor(PointerSensor, { @@ -245,23 +292,52 @@ export const ImageUploadStep = ({ // Get the current product const product = newData[productIndex]; - // Get current image URLs - let currentUrls = product.image_url ? - (typeof product.image_url === 'string' ? product.image_url.split(',') : product.image_url) - : []; + // We need to update product_images array directly instead of the image_url field + if (!product.product_images) { + product.product_images = []; + } else if (typeof product.product_images === 'string') { + // Handle case where it might be a comma-separated string + product.product_images = product.product_images.split(',').filter(Boolean); + } - // Filter out all instances of the URL we're removing - currentUrls = currentUrls.filter((url: string) => url && url !== imageUrl); + // Filter out the image URL we're removing + if (Array.isArray(product.product_images)) { + product.product_images = product.product_images.filter((url: string) => url && url !== imageUrl); + } - // Update the product - product.image_url = currentUrls.join(','); + return newData; + }; + + // Function to add an image URL to a product + const addImageToProduct = (productIndex: number, imageUrl: string) => { + // Create a copy of the data + const newData = [...data]; + + // Get the current product + const product = newData[productIndex]; + + // Initialize product_images array if it doesn't exist + if (!product.product_images) { + product.product_images = []; + } else if (typeof product.product_images === 'string') { + // Handle case where it might be a comma-separated string + product.product_images = product.product_images.split(',').filter(Boolean); + } + + // Ensure it's an array + if (!Array.isArray(product.product_images)) { + product.product_images = [product.product_images].filter(Boolean); + } + + // Only add if the URL doesn't already exist + if (!product.product_images.includes(imageUrl)) { + product.product_images.push(imageUrl); + } - // This is important - actually update the data reference in the parent component - // by passing the newData back to onSubmit, which will update the parent state return newData; }; - // Handle drag end event to reorder or reassign images + // Update handleDragEnd to work with the updated product data structure const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; @@ -358,7 +434,7 @@ export const ImageUploadStep = ({ targetImagesAfter: filteredItems.filter(item => item.productIndex === targetProductIndex).length }); - // Update both products' image_url fields - creating new objects to ensure state updates + // Update both products' image data fields let updatedData = [...data]; // Start with a fresh copy // First remove from source @@ -406,7 +482,7 @@ export const ImageUploadStep = ({ newItems.push(...newFilteredItems); // Update the product data with the new image order - + // Since we're just reordering, the URLs don't change, but their order might matter return newItems; } @@ -427,7 +503,7 @@ export const ImageUploadStep = ({ newItems.push(...newFilteredItems); // Update the product data with the new image order - + // The order might matter for display purposes return newItems; }); } @@ -437,45 +513,7 @@ export const ImageUploadStep = ({ setActiveImage(null); }; - // Function to add an image URL to a product - const addImageToProduct = (productIndex: number, imageUrl: string) => { - // Create a copy of the data - const newData = [...data]; - - // Get the current product - const product = newData[productIndex]; - - // Get the current image URLs or initialize as empty array - let currentUrls = product.image_url ? - (typeof product.image_url === 'string' ? product.image_url.split(',') : product.image_url) - : []; - - // If it's not an array, convert to array - if (!Array.isArray(currentUrls)) { - currentUrls = [currentUrls]; - } - - // Filter out empty values and make sure the URL doesn't already exist - currentUrls = currentUrls.filter((url: string) => url); - - // Only add if the URL doesn't already exist - if (!currentUrls.includes(imageUrl)) { - // Add the new URL - currentUrls.push(imageUrl); - } - - // Update the product - product.image_url = currentUrls.join(','); - - // Update the data - newData[productIndex] = product; - - return newData; - }; - - // Function to update product data with the new image order - - // Function to handle image upload + // Function to handle image upload - update product data const handleImageUpload = async (files: FileList | File[], productIndex: number) => { if (!files || files.length === 0) return; @@ -523,6 +561,7 @@ export const ImageUploadStep = ({ ); // Update the product data with the new image URL + addImageToProduct(productIndex, result.imageUrl); toast.success(`Image uploaded for ${data[productIndex].name || `Product #${productIndex + 1}`}`); } catch (error) { @@ -719,7 +758,7 @@ export const ImageUploadStep = ({ }; }, []); - // Function to remove an image + // Function to remove an image - update to work with product_images const removeImage = async (imageIndex: number) => { const image = productImages[imageIndex]; if (!image) return; @@ -749,6 +788,7 @@ export const ImageUploadStep = ({ setProductImages(prev => prev.filter((_, idx) => idx !== imageIndex)); // Remove the image URL from the product data + removeImageFromProduct(image.productIndex, image.imageUrl); toast.success('Image removed successfully'); } catch (error) { @@ -761,14 +801,30 @@ export const ImageUploadStep = ({ const handleSubmit = useCallback(async () => { setIsSubmitting(true); try { - await onSubmit(data, file); + // First, we need to ensure product_images is properly formatted for each product + const updatedData = [...data].map((product, index) => { + // Get all images for this product + const images = productImages + .filter(img => img.productIndex === index) + .map(img => img.imageUrl) + .filter(Boolean); + + // Update the product with the formatted image URLs + return { + ...product, + // Store as comma-separated string to ensure compatibility + product_images: images.join(',') + }; + }); + + await onSubmit(updatedData, file); } catch (error) { console.error('Submit error:', error); toast.error(`Failed to submit: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsSubmitting(false); } - }, [data, file, onSubmit]); + }, [data, file, onSubmit, productImages]); // Function to ensure URLs are properly formatted with absolute paths const getFullImageUrl = (url: string): string => { @@ -1173,13 +1229,140 @@ export const ImageUploadStep = ({ ); }; + // Handle adding an image from a URL + const handleAddImageFromUrl = async (productIndex: number, url: string) => { + if (!url || !url.trim()) { + toast.error("Please enter a valid URL"); + return; + } + + try { + // Set processing state + setProcessingUrls(prev => ({ ...prev, [productIndex]: true })); + + // Create a unique ID for this image + const imageId = `image-${productIndex}-url-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + + // Add a placeholder for this image while it's being processed + const newImage: ProductImageSortable = { + id: imageId, + productIndex, + imageUrl: url, // Use the URL directly initially + loading: true, + fileName: "From URL" + }; + + setProductImages(prev => [...prev, newImage]); + + // Call the API to validate and potentially process the URL + const response = await fetch(`${config.apiUrl}/import/add-image-from-url`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + url: url, + productIndex: productIndex, + upc: data[productIndex].upc || '', + supplier_no: data[productIndex].supplier_no || '' + }), + }); + + if (!response.ok) { + throw new Error('Failed to add image from URL'); + } + + const result = await response.json(); + + // Update the image URL in our state + setProductImages(prev => + prev.map(img => + img.id === imageId + ? { ...img, imageUrl: result.imageUrl || url, loading: false } + : img + ) + ); + + // Update the product data with the new image URL + addImageToProduct(productIndex, result.imageUrl || url); + + // Clear the URL input field on success + setUrlInputs(prev => ({ ...prev, [productIndex]: '' })); + + toast.success(`Image added from URL for ${data[productIndex].name || `Product #${productIndex + 1}`}`); + } catch (error) { + console.error('Add image from URL error:', error); + + // Remove the failed image from our state + setProductImages(prev => + prev.filter(img => + !(img.loading && img.productIndex === productIndex && img.fileName === "From URL") + ) + ); + + toast.error(`Failed to add image from URL: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { + setProcessingUrls(prev => ({ ...prev, [productIndex]: false })); + } + }; + + // Update the URL input value + const updateUrlInput = (productIndex: number, value: string) => { + setUrlInputs(prev => ({ ...prev, [productIndex]: value })); + }; + + // Add a URL input component + const ImageUrlInput = ({ productIndex }: { productIndex: number }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + + +
+

Add image from URL

+
+ updateUrlInput(productIndex, e.target.value)} + className="text-sm" + /> + +
+
+
+
+ ); + }; + return (
{/* Header - fixed at top */}

Add Product Images

- Upload images for each product. Drag images to reorder them or move them between products. + Upload images for each product or add them from URLs. Drag images to reorder them or move them between products.

@@ -1235,16 +1418,23 @@ export const ImageUploadStep = ({ >
-
-

{product.name || `Product #${index + 1}`}

-
- UPC: {product.upc || 'N/A'} | - Supplier #: {product.supplier_no || 'N/A'} +
+
+

{product.name || `Product #${index + 1}`}

+
+ UPC: {product.upc || 'N/A'} | + Supplier #: {product.supplier_no || 'N/A'} +
+
+
+
-
- +
+
+ +