full bulk product import support
This commit is contained in:
parent
5d0ce42592
commit
3bab4e4c56
1 changed files with 487 additions and 11 deletions
|
|
@ -21,19 +21,27 @@ import {
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle
|
DialogTitle,
|
||||||
|
Stack,
|
||||||
|
Tooltip
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Edit as EditIcon,
|
Edit as EditIcon,
|
||||||
Delete as DeleteIcon,
|
Delete as DeleteIcon,
|
||||||
Add as AddIcon,
|
Add as AddIcon,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
Clear as ClearIcon
|
Clear as ClearIcon,
|
||||||
|
FileDownload as DownloadIcon,
|
||||||
|
Upload as UploadIcon,
|
||||||
|
CloudUpload as CloudUploadIcon,
|
||||||
|
CheckCircleOutline as CheckCircleIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import apiClient from '../../services/api';
|
import apiClient from '../../services/api';
|
||||||
import ProductImage from '../../components/ProductImage';
|
import ProductImage from '../../components/ProductImage';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import Papa from 'papaparse';
|
||||||
|
|
||||||
const AdminProductsPage = () => {
|
const AdminProductsPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -44,6 +52,18 @@ const AdminProductsPage = () => {
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [productToDelete, setProductToDelete] = useState(null);
|
const [productToDelete, setProductToDelete] = useState(null);
|
||||||
|
|
||||||
|
// New states for upload functionality
|
||||||
|
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||||
|
const [uploadFile, setUploadFile] = useState(null);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const [uploadError, setUploadError] = useState(null);
|
||||||
|
const [parsedProducts, setParsedProducts] = useState([]);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [uploadSuccess, setUploadSuccess] = useState(false);
|
||||||
|
const [uploadResults, setUploadResults] = useState(null);
|
||||||
|
|
||||||
|
const fileInputRef = React.useRef(null);
|
||||||
|
|
||||||
// Fetch products
|
// Fetch products
|
||||||
const {
|
const {
|
||||||
data: products,
|
data: products,
|
||||||
|
|
@ -76,6 +96,23 @@ const AdminProductsPage = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bulk upload mutation
|
||||||
|
const bulkUploadProducts = useMutation({
|
||||||
|
mutationFn: async (productData) => {
|
||||||
|
return await apiClient.post('/admin/products', { products: productData });
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin-products'] });
|
||||||
|
setUploadSuccess(true);
|
||||||
|
setUploadResults(data.data);
|
||||||
|
setIsUploading(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setUploadError(error.message || 'Failed to upload products');
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Handle search change
|
// Handle search change
|
||||||
const handleSearchChange = (event) => {
|
const handleSearchChange = (event) => {
|
||||||
setSearch(event.target.value);
|
setSearch(event.target.value);
|
||||||
|
|
@ -123,6 +160,237 @@ const AdminProductsPage = () => {
|
||||||
navigate(`/admin/products/${productId}`);
|
navigate(`/admin/products/${productId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle download template
|
||||||
|
const handleDownloadTemplate = () => {
|
||||||
|
// Create template headers
|
||||||
|
const headers = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'categoryName',
|
||||||
|
'price',
|
||||||
|
'stockQuantity',
|
||||||
|
'weightGrams',
|
||||||
|
'lengthCm',
|
||||||
|
'widthCm',
|
||||||
|
'heightCm',
|
||||||
|
'origin',
|
||||||
|
'age',
|
||||||
|
'materialType',
|
||||||
|
'color',
|
||||||
|
'stockNotification_enabled',
|
||||||
|
'stockNotification_email',
|
||||||
|
'stockNotification_threshold',
|
||||||
|
'tags' // Comma-separated tags
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create example data row
|
||||||
|
const exampleRow = [
|
||||||
|
'Example Product',
|
||||||
|
'This is a product description',
|
||||||
|
'Rock', // Valid category name
|
||||||
|
'19.99',
|
||||||
|
'10',
|
||||||
|
'500',
|
||||||
|
'15',
|
||||||
|
'10',
|
||||||
|
'5',
|
||||||
|
'Brazil',
|
||||||
|
'Recent',
|
||||||
|
'Quartz',
|
||||||
|
'Purple',
|
||||||
|
'FALSE', // stockNotification_enabled
|
||||||
|
'email@example.com', // stockNotification_email
|
||||||
|
'5', // stockNotification_threshold
|
||||||
|
'Rare,Polished' // Comma-separated tags
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create workbook
|
||||||
|
const worksheet = XLSX.utils.aoa_to_sheet([headers, exampleRow]);
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Products Template');
|
||||||
|
|
||||||
|
// Add column widths for better readability
|
||||||
|
const colWidths = [];
|
||||||
|
headers.forEach(() => colWidths.push({ wch: 15 }));
|
||||||
|
worksheet['!cols'] = colWidths;
|
||||||
|
|
||||||
|
// Generate Excel file
|
||||||
|
XLSX.writeFile(workbook, 'products_upload_template.xlsx');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle open upload dialog
|
||||||
|
const handleOpenUploadDialog = () => {
|
||||||
|
resetUploadState();
|
||||||
|
setUploadDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle close upload dialog
|
||||||
|
const handleCloseUploadDialog = () => {
|
||||||
|
resetUploadState();
|
||||||
|
setUploadDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset upload states
|
||||||
|
const resetUploadState = () => {
|
||||||
|
setUploadFile(null);
|
||||||
|
setUploadProgress(0);
|
||||||
|
setUploadError(null);
|
||||||
|
setParsedProducts([]);
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadSuccess(false);
|
||||||
|
setUploadResults(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle file selection
|
||||||
|
const handleFileSelect = (e) => {
|
||||||
|
setUploadError(null);
|
||||||
|
setUploadSuccess(false);
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setUploadFile(file);
|
||||||
|
|
||||||
|
// Determine file type and parse accordingly
|
||||||
|
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
if (fileExtension === 'csv') {
|
||||||
|
parseCsvFile(file);
|
||||||
|
} else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
|
||||||
|
parseExcelFile(file);
|
||||||
|
} else {
|
||||||
|
setUploadError('Unsupported file format. Please upload a CSV or Excel file.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse CSV file
|
||||||
|
const parseCsvFile = (file) => {
|
||||||
|
Papa.parse(file, {
|
||||||
|
header: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
complete: (results) => {
|
||||||
|
if (results.errors.length > 0) {
|
||||||
|
setUploadError(`Error parsing CSV: ${results.errors[0].message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedProducts = formatParsedProducts(results.data);
|
||||||
|
setParsedProducts(formattedProducts);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
setUploadError(`Error parsing CSV: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse Excel file
|
||||||
|
const parseExcelFile = (file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = new Uint8Array(e.target.result);
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' });
|
||||||
|
|
||||||
|
// Get first sheet
|
||||||
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
|
const worksheet = workbook.Sheets[firstSheetName];
|
||||||
|
|
||||||
|
// Convert to JSON
|
||||||
|
const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: null });
|
||||||
|
|
||||||
|
const formattedProducts = formatParsedProducts(jsonData);
|
||||||
|
setParsedProducts(formattedProducts);
|
||||||
|
} catch (error) {
|
||||||
|
setUploadError(`Error parsing Excel file: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
setUploadError('Error reading file');
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format parsed products to match API expectations
|
||||||
|
const formatParsedProducts = (rawProducts) => {
|
||||||
|
return rawProducts.map(product => {
|
||||||
|
// Extract stock notification fields and combine them
|
||||||
|
const stockNotification = {
|
||||||
|
enabled: product.stockNotification_enabled === 'TRUE' || product.stockNotification_enabled === true,
|
||||||
|
email: product.stockNotification_email || null,
|
||||||
|
threshold: product.stockNotification_threshold ? parseInt(product.stockNotification_threshold) : null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process tags (convert from comma-separated string to array)
|
||||||
|
let tags = [];
|
||||||
|
if (product.tags) {
|
||||||
|
if (typeof product.tags === 'string') {
|
||||||
|
tags = product.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||||
|
} else if (Array.isArray(product.tags)) {
|
||||||
|
tags = product.tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert numeric values
|
||||||
|
const price = product.price ? parseFloat(product.price) : null;
|
||||||
|
const stockQuantity = product.stockQuantity ? parseInt(product.stockQuantity) : null;
|
||||||
|
const weightGrams = product.weightGrams ? parseFloat(product.weightGrams) : null;
|
||||||
|
const lengthCm = product.lengthCm ? parseFloat(product.lengthCm) : null;
|
||||||
|
const widthCm = product.widthCm ? parseFloat(product.widthCm) : null;
|
||||||
|
const heightCm = product.heightCm ? parseFloat(product.heightCm) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: product.name || '',
|
||||||
|
description: product.description || '',
|
||||||
|
categoryName: product.categoryName || '',
|
||||||
|
price: price,
|
||||||
|
stockQuantity: stockQuantity,
|
||||||
|
weightGrams: weightGrams,
|
||||||
|
lengthCm: lengthCm,
|
||||||
|
widthCm: widthCm,
|
||||||
|
heightCm: heightCm,
|
||||||
|
origin: product.origin || null,
|
||||||
|
age: product.age || null,
|
||||||
|
materialType: product.materialType || null,
|
||||||
|
color: product.color || null,
|
||||||
|
stockNotification: stockNotification,
|
||||||
|
tags: tags,
|
||||||
|
images: [] // No images in bulk upload template
|
||||||
|
};
|
||||||
|
}).filter(product =>
|
||||||
|
// Filter out products with missing required fields
|
||||||
|
product.name &&
|
||||||
|
product.description &&
|
||||||
|
product.categoryName &&
|
||||||
|
product.price !== null &&
|
||||||
|
product.stockQuantity !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit bulk upload
|
||||||
|
const handleSubmitUpload = () => {
|
||||||
|
if (parsedProducts.length === 0) {
|
||||||
|
setUploadError('No valid products found in the file. Please check your data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
setUploadError(null);
|
||||||
|
|
||||||
|
// Submit products in batches of 20 to respect connection limits
|
||||||
|
const batchSize = 20;
|
||||||
|
const batches = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < parsedProducts.length; i += batchSize) {
|
||||||
|
const batch = parsedProducts.slice(i, i + batchSize);
|
||||||
|
batches.push(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process first batch
|
||||||
|
bulkUploadProducts.mutate(parsedProducts);
|
||||||
|
};
|
||||||
|
|
||||||
// Filter and paginate products
|
// Filter and paginate products
|
||||||
const filteredProducts = products || [];
|
const filteredProducts = products || [];
|
||||||
const paginatedProducts = filteredProducts.slice(
|
const paginatedProducts = filteredProducts.slice(
|
||||||
|
|
@ -155,6 +423,29 @@ const AdminProductsPage = () => {
|
||||||
Products
|
Products
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Tooltip title="Download a template for bulk product uploads">
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
onClick={handleDownloadTemplate}
|
||||||
|
>
|
||||||
|
Download Template
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Upload products in bulk from a CSV or Excel file">
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<UploadIcon />}
|
||||||
|
onClick={handleOpenUploadDialog}
|
||||||
|
>
|
||||||
|
Upload Bulk Assets
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -164,6 +455,7 @@ const AdminProductsPage = () => {
|
||||||
>
|
>
|
||||||
Add Product
|
Add Product
|
||||||
</Button>
|
</Button>
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
|
|
@ -301,6 +593,190 @@ const AdminProductsPage = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Upload Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={uploadDialogOpen}
|
||||||
|
onClose={handleCloseUploadDialog}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>Upload Products</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{!uploadSuccess ? (
|
||||||
|
<>
|
||||||
|
<DialogContentText sx={{ mb: 2 }}>
|
||||||
|
Upload a CSV or Excel file containing your product data. Make sure the file follows the required format.
|
||||||
|
</DialogContentText>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Drag and drop area */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
border: '2px dashed',
|
||||||
|
borderColor: theme => uploadFile ? theme.palette.success.main : theme.palette.divider,
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 3,
|
||||||
|
mb: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: theme => uploadFile ? 'rgba(76, 175, 80, 0.08)' : 'transparent',
|
||||||
|
cursor: isUploading ? 'not-allowed' : 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: theme => !isUploading && theme.palette.primary.main,
|
||||||
|
backgroundColor: theme => !isUploading && 'rgba(25, 118, 210, 0.04)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
component="label"
|
||||||
|
onDrop={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (isUploading) return;
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length) {
|
||||||
|
// Update the file input to reflect the dragged file
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
// Create a DataTransfer object
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(files[0]);
|
||||||
|
fileInputRef.current.files = dataTransfer.files;
|
||||||
|
|
||||||
|
// Trigger the change event handler
|
||||||
|
const event = new Event('change', { bubbles: true });
|
||||||
|
fileInputRef.current.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragOver={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isUploading) {
|
||||||
|
e.currentTarget.style.borderColor = '#1976d2';
|
||||||
|
e.currentTarget.style.backgroundColor = 'rgba(25, 118, 210, 0.04)';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragLeave={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!uploadFile) {
|
||||||
|
e.currentTarget.style.borderColor = '';
|
||||||
|
e.currentTarget.style.backgroundColor = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".csv, .xlsx, .xls"
|
||||||
|
hidden
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{uploadFile ? (
|
||||||
|
<>
|
||||||
|
<CheckCircleIcon sx={{ color: 'success.main', fontSize: 48, mb: 1 }} />
|
||||||
|
<Typography variant="body1" align="center" gutterBottom>
|
||||||
|
File selected: {uploadFile.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" align="center">
|
||||||
|
Drag and drop a different file or click to change
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CloudUploadIcon sx={{ fontSize: 48, color: 'action.active', mb: 1 }} />
|
||||||
|
<Typography variant="body1" align="center" gutterBottom>
|
||||||
|
Drag and drop your CSV or Excel file here
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" align="center">
|
||||||
|
or click to select a file
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{parsedProducts.length > 0 && (
|
||||||
|
<Alert severity="info" sx={{ mt: 2, width: '100%' }}>
|
||||||
|
Found {parsedProducts.length} valid products in the file.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploadError && (
|
||||||
|
<Alert severity="error" sx={{ mt: 2, width: '100%' }}>
|
||||||
|
{uploadError}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ mt: 2, mb: 2 }}>
|
||||||
|
<Alert severity="success" sx={{ mb: 2 }}>
|
||||||
|
Upload successful!
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{uploadResults && (
|
||||||
|
<>
|
||||||
|
<Typography variant="subtitle1" sx={{ mt: 2, mb: 1 }}>
|
||||||
|
Upload Results:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2">
|
||||||
|
{uploadResults.success?.length || 0} products created successfully
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{uploadResults.errs?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
||||||
|
{uploadResults.errs.length} products failed to create
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Paper variant="outlined" sx={{ mt: 1, p: 1, maxHeight: 150, overflow: 'auto' }}>
|
||||||
|
{uploadResults.errs.map((err, index) => (
|
||||||
|
<Typography key={index} variant="caption" display="block" color="error">
|
||||||
|
Error: {err.message}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{!uploadSuccess ? (
|
||||||
|
<>
|
||||||
|
<Button onClick={handleCloseUploadDialog} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmitUpload}
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
disabled={isUploading || parsedProducts.length === 0}
|
||||||
|
startIcon={isUploading && <CircularProgress size={20} color="inherit" />}
|
||||||
|
>
|
||||||
|
{isUploading ? 'Uploading...' : 'Upload Products'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button onClick={handleCloseUploadDialog} color="primary">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue