fix: Database schema fixes. CI/CD improvements.
This commit is contained in:
@@ -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