fix: Database schema fixes. CI/CD improvements.
This commit is contained in:
@@ -347,11 +347,10 @@ export const useImportApply = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (previewId: string) => adminApi.importApply(previewId),
|
||||
onSuccess: (result) => {
|
||||
onSuccess: () => {
|
||||
// Invalidate cache to refresh catalog data
|
||||
queryClient.invalidateQueries({ queryKey: ['catalogSearch'] });
|
||||
toast.success(
|
||||
`Import completed: ${result.created} created, ${result.updated} updated`
|
||||
);
|
||||
// Note: Toast and dialog behavior now handled by parent components
|
||||
},
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.error || 'Failed to apply import');
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
MoreVert,
|
||||
Close,
|
||||
History,
|
||||
ExpandMore,
|
||||
ExpandLess,
|
||||
} from '@mui/icons-material';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useAdminAccess } from '../../../core/auth/useAdminAccess';
|
||||
@@ -29,6 +31,7 @@ import { adminApi } from '../api/admin.api';
|
||||
import {
|
||||
CatalogSearchResult,
|
||||
ImportPreviewResult,
|
||||
ImportApplyResult,
|
||||
} from '../types/admin.types';
|
||||
|
||||
export const AdminCatalogMobileScreen: React.FC = () => {
|
||||
@@ -54,6 +57,8 @@ export const AdminCatalogMobileScreen: React.FC = () => {
|
||||
// Import state
|
||||
const [importSheet, setImportSheet] = useState(false);
|
||||
const [importPreview, setImportPreview] = useState<ImportPreviewResult | null>(null);
|
||||
const [importResult, setImportResult] = useState<ImportApplyResult | null>(null);
|
||||
const [errorsExpanded, setErrorsExpanded] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Hooks
|
||||
@@ -144,15 +149,38 @@ export const AdminCatalogMobileScreen: React.FC = () => {
|
||||
if (!importPreview?.previewId) return;
|
||||
|
||||
try {
|
||||
await importApplyMutation.mutateAsync(importPreview.previewId);
|
||||
setImportSheet(false);
|
||||
setImportPreview(null);
|
||||
const result = await importApplyMutation.mutateAsync(importPreview.previewId);
|
||||
setImportResult(result);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
toast.error(
|
||||
`Import completed with ${result.errors.length} error(s): ${result.created} created, ${result.updated} updated`
|
||||
);
|
||||
// Keep sheet open for error review
|
||||
} else {
|
||||
toast.success(
|
||||
`Import completed successfully: ${result.created} created, ${result.updated} updated`
|
||||
);
|
||||
// Auto-close on complete success
|
||||
setImportSheet(false);
|
||||
setImportPreview(null);
|
||||
setImportResult(null);
|
||||
}
|
||||
|
||||
refetch();
|
||||
} catch {
|
||||
// Error handled by mutation
|
||||
// Error handled by mutation's onError
|
||||
}
|
||||
}, [importPreview, importApplyMutation, refetch]);
|
||||
|
||||
const handleImportSheetClose = useCallback(() => {
|
||||
if (importApplyMutation.isPending) return;
|
||||
setImportSheet(false);
|
||||
setImportPreview(null);
|
||||
setImportResult(null);
|
||||
setErrorsExpanded(false);
|
||||
}, [importApplyMutation.isPending]);
|
||||
|
||||
// Export handler
|
||||
const handleExport = useCallback(() => {
|
||||
setMenuOpen(false);
|
||||
@@ -435,17 +463,16 @@ export const AdminCatalogMobileScreen: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import Preview Sheet */}
|
||||
{importSheet && importPreview && (
|
||||
{/* Import Preview/Results Sheet */}
|
||||
{importSheet && (importPreview || importResult) && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-end justify-center">
|
||||
<div className="bg-white rounded-t-2xl w-full max-w-lg p-6 space-y-4 animate-slide-up max-h-[80vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-slate-800">Import Preview</h2>
|
||||
<h2 className="text-xl font-bold text-slate-800">
|
||||
{importResult ? 'Import Results' : 'Import Preview'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setImportSheet(false);
|
||||
setImportPreview(null);
|
||||
}}
|
||||
onClick={handleImportSheetClose}
|
||||
disabled={importApplyMutation.isPending}
|
||||
className="p-2 text-slate-500 hover:text-slate-700"
|
||||
style={{ minHeight: '44px', minWidth: '44px' }}
|
||||
@@ -454,74 +481,127 @@ export const AdminCatalogMobileScreen: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="flex gap-4 text-sm">
|
||||
<div className="bg-green-100 text-green-800 px-3 py-2 rounded-lg">
|
||||
<strong>{importPreview.toCreate.length}</strong> to create
|
||||
</div>
|
||||
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-lg">
|
||||
<strong>{importPreview.toUpdate.length}</strong> to update
|
||||
</div>
|
||||
</div>
|
||||
{/* Preview Mode */}
|
||||
{importPreview && !importResult && (
|
||||
<>
|
||||
<div className="flex gap-4 text-sm">
|
||||
<div className="bg-green-100 text-green-800 px-3 py-2 rounded-lg">
|
||||
<strong>{importPreview.toCreate.length}</strong> to create
|
||||
</div>
|
||||
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-lg">
|
||||
<strong>{importPreview.toUpdate.length}</strong> to update
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Errors */}
|
||||
{importPreview.errors.length > 0 && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<p className="text-red-800 font-semibold mb-2">
|
||||
{importPreview.errors.length} Error(s) Found:
|
||||
</p>
|
||||
<ul className="text-red-700 text-sm space-y-1">
|
||||
{importPreview.errors.slice(0, 5).map((err, idx) => (
|
||||
<li key={idx}>
|
||||
Row {err.row}: {err.error}
|
||||
</li>
|
||||
))}
|
||||
{importPreview.errors.length > 5 && (
|
||||
<li>...and {importPreview.errors.length - 5} more errors</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
{importPreview.errors.length > 0 && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<p className="text-red-800 font-semibold mb-2">
|
||||
{importPreview.errors.length} Error(s) Found:
|
||||
</p>
|
||||
<ul className="text-red-700 text-sm space-y-1">
|
||||
{importPreview.errors.slice(0, 5).map((err, idx) => (
|
||||
<li key={idx}>
|
||||
Row {err.row}: {err.error}
|
||||
</li>
|
||||
))}
|
||||
{importPreview.errors.length > 5 && (
|
||||
<li>...and {importPreview.errors.length - 5} more errors</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importPreview.valid ? (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<p className="text-green-800">
|
||||
The import file is valid and ready to be applied.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p className="text-amber-800">
|
||||
Please fix the errors above before importing.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
{importPreview.valid ? (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<p className="text-green-800">
|
||||
The import file is valid and ready to be applied.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p className="text-amber-800">
|
||||
Please fix the errors above before importing.
|
||||
</p>
|
||||
</div>
|
||||
{/* Results Mode */}
|
||||
{importResult && (
|
||||
<>
|
||||
<div className="flex gap-4 text-sm">
|
||||
<div className="bg-green-100 text-green-800 px-3 py-2 rounded-lg">
|
||||
<strong>{importResult.created}</strong> created
|
||||
</div>
|
||||
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-lg">
|
||||
<strong>{importResult.updated}</strong> updated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importResult.errors.length > 0 && (
|
||||
<div className="border border-red-500 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setErrorsExpanded(!errorsExpanded)}
|
||||
className="w-full flex items-center justify-between p-4 bg-red-100 hover:bg-red-200 transition"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
<span className="text-red-900 font-semibold">
|
||||
{importResult.errors.length} Error(s) Occurred
|
||||
</span>
|
||||
<span className="text-red-900">
|
||||
{errorsExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{errorsExpanded && (
|
||||
<div className="max-h-96 overflow-y-auto p-4 bg-white">
|
||||
<ul className="space-y-2">
|
||||
{importResult.errors.map((err, idx) => (
|
||||
<li key={idx} className="text-sm font-mono text-slate-700">
|
||||
<strong>Row {err.row}:</strong> {err.error}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importResult.errors.length === 0 && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<p className="text-green-800">
|
||||
Import completed successfully with no errors.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setImportSheet(false);
|
||||
setImportPreview(null);
|
||||
}}
|
||||
onClick={handleImportSheetClose}
|
||||
disabled={importApplyMutation.isPending}
|
||||
className="flex-1 bg-slate-200 text-slate-700 py-3 rounded-lg font-medium hover:bg-slate-300 transition disabled:opacity-50"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImportConfirm}
|
||||
disabled={!importPreview.valid || importApplyMutation.isPending}
|
||||
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition disabled:opacity-50"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{importApplyMutation.isPending ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto" />
|
||||
) : (
|
||||
'Apply Import'
|
||||
)}
|
||||
{importResult ? 'Close' : 'Cancel'}
|
||||
</button>
|
||||
{!importResult && (
|
||||
<button
|
||||
onClick={handleImportConfirm}
|
||||
disabled={!importPreview?.valid || importApplyMutation.isPending}
|
||||
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition disabled:opacity-50"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{importApplyMutation.isPending ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto" />
|
||||
) : (
|
||||
'Apply Import'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
Alert,
|
||||
Collapse,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search,
|
||||
@@ -35,6 +36,8 @@ import {
|
||||
FileDownload,
|
||||
FileUpload,
|
||||
Clear,
|
||||
ExpandMore,
|
||||
ExpandLess,
|
||||
} from '@mui/icons-material';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useAdminAccess } from '../../core/auth/useAdminAccess';
|
||||
@@ -52,6 +55,7 @@ import {
|
||||
import {
|
||||
CatalogSearchResult,
|
||||
ImportPreviewResult,
|
||||
ImportApplyResult,
|
||||
} from '../../features/admin/types/admin.types';
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [25, 50, 100];
|
||||
@@ -76,6 +80,8 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
// Import state
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
const [importPreview, setImportPreview] = useState<ImportPreviewResult | null>(null);
|
||||
const [importResult, setImportResult] = useState<ImportApplyResult | null>(null);
|
||||
const [errorsExpanded, setErrorsExpanded] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Hooks
|
||||
@@ -217,15 +223,38 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
if (!importPreview?.previewId) return;
|
||||
|
||||
try {
|
||||
await importApplyMutation.mutateAsync(importPreview.previewId);
|
||||
setImportDialogOpen(false);
|
||||
setImportPreview(null);
|
||||
const result = await importApplyMutation.mutateAsync(importPreview.previewId);
|
||||
setImportResult(result);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
toast.error(
|
||||
`Import completed with ${result.errors.length} error(s): ${result.created} created, ${result.updated} updated`
|
||||
);
|
||||
// Keep dialog open for error review
|
||||
} else {
|
||||
toast.success(
|
||||
`Import completed successfully: ${result.created} created, ${result.updated} updated`
|
||||
);
|
||||
// Auto-close on complete success
|
||||
setImportDialogOpen(false);
|
||||
setImportPreview(null);
|
||||
setImportResult(null);
|
||||
}
|
||||
|
||||
refetch();
|
||||
} catch (error) {
|
||||
// Error is handled by mutation
|
||||
// Error is handled by mutation's onError
|
||||
}
|
||||
}, [importPreview, importApplyMutation, refetch]);
|
||||
|
||||
const handleImportDialogClose = useCallback(() => {
|
||||
if (importApplyMutation.isPending) return;
|
||||
setImportDialogOpen(false);
|
||||
setImportPreview(null);
|
||||
setImportResult(null);
|
||||
setErrorsExpanded(false);
|
||||
}, [importApplyMutation.isPending]);
|
||||
|
||||
// Export handler
|
||||
const handleExport = useCallback(() => {
|
||||
exportMutation.mutate();
|
||||
@@ -506,18 +535,20 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Import Preview Dialog */}
|
||||
{/* Import Preview/Results Dialog */}
|
||||
<Dialog
|
||||
open={importDialogOpen}
|
||||
onClose={() => !importApplyMutation.isPending && setImportDialogOpen(false)}
|
||||
onClose={handleImportDialogClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Import Preview</DialogTitle>
|
||||
<DialogTitle>
|
||||
{importResult ? 'Import Results' : 'Import Preview'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{importPreview && (
|
||||
{/* Preview Mode */}
|
||||
{importPreview && !importResult && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
||||
{/* Summary */}
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Typography>
|
||||
<strong>To Create:</strong> {importPreview.toCreate.length}
|
||||
@@ -527,7 +558,6 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Errors */}
|
||||
{importPreview.errors.length > 0 && (
|
||||
<Alert severity="error">
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
@@ -546,7 +576,6 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Valid status */}
|
||||
{importPreview.valid ? (
|
||||
<Alert severity="success">
|
||||
The import file is valid and ready to be applied.
|
||||
@@ -558,23 +587,86 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results Mode */}
|
||||
{importResult && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Typography>
|
||||
<strong>Created:</strong> {importResult.created}
|
||||
</Typography>
|
||||
<Typography>
|
||||
<strong>Updated:</strong> {importResult.updated}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{importResult.errors.length > 0 && (
|
||||
<Box sx={{ border: 1, borderColor: 'error.main', borderRadius: 1 }}>
|
||||
<Box
|
||||
onClick={() => setErrorsExpanded(!errorsExpanded)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
p: 2,
|
||||
bgcolor: 'error.light',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: 'error.main', color: 'white' },
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{importResult.errors.length} Error(s) Occurred
|
||||
</Typography>
|
||||
<IconButton size="small" sx={{ color: 'inherit' }}>
|
||||
{errorsExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Collapse in={errorsExpanded}>
|
||||
<Box sx={{ maxHeight: 400, overflow: 'auto', p: 2, bgcolor: 'background.paper' }}>
|
||||
<Box component="ul" sx={{ m: 0, pl: 2 }}>
|
||||
{importResult.errors.map((err, idx) => (
|
||||
<Typography
|
||||
component="li"
|
||||
key={idx}
|
||||
variant="body2"
|
||||
sx={{ mb: 1, fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||
>
|
||||
<strong>Row {err.row}:</strong> {err.error}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{importResult.errors.length === 0 && (
|
||||
<Alert severity="success">
|
||||
Import completed successfully with no errors.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => setImportDialogOpen(false)}
|
||||
onClick={handleImportDialogClose}
|
||||
disabled={importApplyMutation.isPending}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImportConfirm}
|
||||
disabled={!importPreview?.valid || importApplyMutation.isPending}
|
||||
variant="contained"
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
{importApplyMutation.isPending ? <CircularProgress size={20} /> : 'Apply Import'}
|
||||
{importResult ? 'Close' : 'Cancel'}
|
||||
</Button>
|
||||
{!importResult && (
|
||||
<Button
|
||||
onClick={handleImportConfirm}
|
||||
disabled={!importPreview?.valid || importApplyMutation.isPending}
|
||||
variant="contained"
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
{importApplyMutation.isPending ? <CircularProgress size={20} /> : 'Apply Import'}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user