Readur/frontend/src/components/Labels/LabelCreateDialog.tsx

328 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Box,
Typography,
IconButton,
Paper,
Tooltip,
} from '@mui/material';
import Grid from '@mui/material/GridLegacy';
import {
Star as StarIcon,
Archive as ArchiveIcon,
Person as PersonIcon,
Work as WorkIcon,
Receipt as ReceiptIcon,
Scale as ScaleIcon,
LocalHospital as MedicalIcon,
AttachMoney as DollarIcon,
BusinessCenter as BriefcaseIcon,
Description as DocumentIcon,
Label as LabelIcon,
BugReport as BugIcon,
Build as BuildIcon,
Folder as FolderIcon,
Assignment as AssignmentIcon,
Schedule as ScheduleIcon,
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import Label, { type LabelData } from './Label';
interface LabelCreateDialogProps {
open: boolean;
onClose: () => void;
onSubmit: (labelData: Omit<LabelData, 'id' | 'is_system' | 'created_at' | 'updated_at' | 'document_count' | 'source_count'>) => Promise<void>;
prefilledName?: string;
editingLabel?: LabelData;
}
const availableIcons = [
{ name: 'star', icon: StarIcon, label: 'Star' },
{ name: 'archive', icon: ArchiveIcon, label: 'Archive' },
{ name: 'person', icon: PersonIcon, label: 'Person' },
{ name: 'work', icon: WorkIcon, label: 'Work' },
{ name: 'briefcase', icon: BriefcaseIcon, label: 'Briefcase' },
{ name: 'receipt', icon: ReceiptIcon, label: 'Receipt' },
{ name: 'scale', icon: ScaleIcon, label: 'Legal' },
{ name: 'medical', icon: MedicalIcon, label: 'Medical' },
{ name: 'dollar', icon: DollarIcon, label: 'Money' },
{ name: 'document', icon: DocumentIcon, label: 'Document' },
{ name: 'label', icon: LabelIcon, label: 'Label' },
{ name: 'bug', icon: BugIcon, label: 'Bug' },
{ name: 'build', icon: BuildIcon, label: 'Build' },
{ name: 'folder', icon: FolderIcon, label: 'Folder' },
{ name: 'assignment', icon: AssignmentIcon, label: 'Assignment' },
{ name: 'schedule', icon: ScheduleIcon, label: 'Schedule' },
];
const predefinedColors = [
'#0969da', // GitHub blue
'#d73a49', // GitHub red
'#28a745', // GitHub green
'#ffd33d', // GitHub yellow
'#8250df', // GitHub purple
'#fd7e14', // Orange
'#20c997', // Teal
'#6f42c1', // Indigo
'#e83e8c', // Pink
'#6c757d', // Gray
];
const LabelCreateDialog: React.FC<LabelCreateDialogProps> = ({
open,
onClose,
onSubmit,
prefilledName = '',
editingLabel
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState({
name: '',
description: '',
color: '#0969da',
background_color: '',
icon: '',
});
const [loading, setLoading] = useState(false);
const [nameError, setNameError] = useState('');
useEffect(() => {
if (editingLabel) {
setFormData({
name: editingLabel.name,
description: editingLabel.description || '',
color: editingLabel.color,
background_color: editingLabel.background_color || '',
icon: editingLabel.icon || '',
});
} else {
setFormData({
name: prefilledName,
description: '',
color: '#0969da',
background_color: '',
icon: '',
});
}
setNameError('');
}, [editingLabel, prefilledName, open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
setNameError(t('labels.errors.invalidName'));
return;
}
setLoading(true);
try {
await onSubmit({
name: formData.name.trim(),
description: formData.description.trim() || undefined,
color: formData.color,
background_color: formData.background_color || undefined,
icon: formData.icon || undefined,
});
handleClose();
} catch (error) {
console.error('Failed to save label:', error);
// Could add error handling UI here
} finally {
setLoading(false);
}
};
const handleClose = () => {
if (!loading) {
onClose();
}
};
const previewLabel: LabelData = {
id: 'preview',
name: formData.name || 'Label Preview',
description: formData.description,
color: formData.color,
background_color: formData.background_color,
icon: formData.icon,
is_system: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
document_count: 0,
source_count: 0,
};
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
PaperProps={{
component: 'form',
onSubmit: handleSubmit,
}}
>
<DialogTitle>
{editingLabel ? t('labels.create.editTitle') : t('labels.create.title')}
</DialogTitle>
<DialogContent sx={{ pt: 2 }}>
<Grid container spacing={3}>
{/* Name Field */}
<Grid item xs={12}>
<TextField
label={t('labels.create.nameLabel')}
value={formData.name}
onChange={(e) => {
setFormData({ ...formData, name: e.target.value });
if (nameError) setNameError('');
}}
error={!!nameError}
helperText={nameError}
fullWidth
required
autoFocus
disabled={loading}
/>
</Grid>
{/* Description Field */}
<Grid item xs={12}>
<TextField
label={t('labels.create.descriptionLabel')}
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
fullWidth
multiline
rows={2}
disabled={loading}
/>
</Grid>
{/* Color Selection */}
<Grid item xs={12}>
<Typography variant="subtitle2" gutterBottom>
{t('labels.create.colorLabel')}
</Typography>
<Box display="flex" flexWrap="wrap" gap={1} mb={2}>
{predefinedColors.map((color) => (
<IconButton
key={color}
onClick={() => setFormData({ ...formData, color })}
disabled={loading}
sx={{
width: 32,
height: 32,
backgroundColor: color,
border: formData.color === color ? '3px solid' : '1px solid',
borderColor: formData.color === color ? 'primary.main' : 'divider',
'&:hover': {
backgroundColor: color,
opacity: 0.8,
},
}}
/>
))}
</Box>
<TextField
label={t('labels.create.customColorLabel')}
value={formData.color}
onChange={(e) => setFormData({ ...formData, color: e.target.value })}
size="small"
disabled={loading}
InputProps={{
startAdornment: (
<Box
sx={{
width: 20,
height: 20,
backgroundColor: formData.color,
border: '1px solid',
borderColor: 'divider',
borderRadius: 0.5,
mr: 1,
}}
/>
),
}}
/>
</Grid>
{/* Icon Selection */}
<Grid item xs={12}>
<Typography variant="subtitle2" gutterBottom>
{t('labels.create.iconLabel')}
</Typography>
<Box display="flex" flexWrap="wrap" gap={1}>
<IconButton
onClick={() => setFormData({ ...formData, icon: '' })}
disabled={loading}
sx={{
border: '1px solid',
borderColor: !formData.icon ? 'primary.main' : 'divider',
backgroundColor: !formData.icon ? 'action.selected' : 'transparent',
}}
>
<Typography variant="caption">{t('labels.create.iconNone')}</Typography>
</IconButton>
{availableIcons.map((iconData) => {
const IconComponent = iconData.icon;
return (
<Tooltip key={iconData.name} title={iconData.label}>
<IconButton
onClick={() => setFormData({ ...formData, icon: iconData.name })}
disabled={loading}
sx={{
border: '1px solid',
borderColor: formData.icon === iconData.name ? 'primary.main' : 'divider',
backgroundColor: formData.icon === iconData.name ? 'action.selected' : 'transparent',
}}
>
<IconComponent fontSize="small" />
</IconButton>
</Tooltip>
);
})}
</Box>
</Grid>
{/* Preview */}
<Grid item xs={12}>
<Typography variant="subtitle2" gutterBottom>
{t('labels.create.previewLabel')}
</Typography>
<Paper sx={{ p: 2, backgroundColor: 'grey.50' }}>
<Box display="flex" gap={1} flexWrap="wrap">
<Label label={previewLabel} variant="filled" />
<Label label={previewLabel} variant="outlined" />
</Box>
</Paper>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('labels.create.cancel')}
</Button>
<Button
type="submit"
variant="contained"
disabled={loading || !formData.name.trim()}
>
{loading ? t('labels.create.saving') : (editingLabel ? t('labels.create.update') : t('labels.create.create'))}
</Button>
</DialogActions>
</Dialog>
);
};
export default LabelCreateDialog;