feat: Add SMTP settings and collapsible email configuration UI
- Add SMTP fields to TicketEmailSettings model (host, port, TLS/SSL, credentials, from email/name) - Update serializers with SMTP fields and is_smtp_configured flag - Add TicketEmailTestSmtpView for testing SMTP connections - Update frontend API types and hooks for SMTP settings - Add collapsible IMAP and SMTP configuration sections with "Configured" badges - Fix TypeScript errors in mockData.ts (missing required fields, type mismatches) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
112
frontend/src/components/EmailTemplateSelector.tsx
Normal file
112
frontend/src/components/EmailTemplateSelector.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Mail, ExternalLink } from 'lucide-react';
|
||||
import api from '../api/client';
|
||||
import { EmailTemplate } from '../types';
|
||||
|
||||
interface EmailTemplateSelectorProps {
|
||||
value: string | number | undefined;
|
||||
onChange: (templateId: string | number | undefined) => void;
|
||||
category?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const EmailTemplateSelector: React.FC<EmailTemplateSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
category,
|
||||
placeholder,
|
||||
required = false,
|
||||
disabled = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Fetch email templates
|
||||
const { data: templates = [], isLoading } = useQuery<EmailTemplate[]>({
|
||||
queryKey: ['email-templates-list', category],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (category) params.append('category', category);
|
||||
const { data } = await api.get(`/api/email-templates/?${params.toString()}`);
|
||||
return data.map((t: any) => ({
|
||||
id: String(t.id),
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
category: t.category,
|
||||
scope: t.scope,
|
||||
updatedAt: t.updated_at,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
const selectedTemplate = templates.find(t => String(t.id) === String(value));
|
||||
|
||||
return (
|
||||
<div className={`space-y-2 ${className}`}>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value || undefined)}
|
||||
disabled={disabled || isLoading}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500 disabled:opacity-50 disabled:cursor-not-allowed appearance-none"
|
||||
>
|
||||
<option value="">
|
||||
{isLoading
|
||||
? t('common.loading', 'Loading...')
|
||||
: placeholder || t('emailTemplates.selectTemplate', 'Select a template...')}
|
||||
</option>
|
||||
{templates.map((template) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
{template.category !== 'OTHER' && ` (${template.category})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 pointer-events-none" />
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected template info */}
|
||||
{selectedTemplate && (
|
||||
<div className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-800 rounded text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400 truncate">
|
||||
{selectedTemplate.description || selectedTemplate.name}
|
||||
</span>
|
||||
<a
|
||||
href={`#/email-templates`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-brand-600 dark:text-brand-400 hover:underline ml-2 flex-shrink-0"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
{t('common.edit', 'Edit')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state with link to create */}
|
||||
{!isLoading && templates.length === 0 && (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('emailTemplates.noTemplatesYet', 'No email templates yet.')}{' '}
|
||||
<a
|
||||
href="#/email-templates"
|
||||
className="text-brand-600 dark:text-brand-400 hover:underline"
|
||||
>
|
||||
{t('emailTemplates.createFirst', 'Create your first template')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailTemplateSelector;
|
||||
Reference in New Issue
Block a user