Fix email template preview UX and debug template loading
- Convert EmailTemplatePreview from bottom card to modal Dialog component - Replace problematic bottom preview with clean modal popup - Add proper modal state management (open/close handlers) - Debug template loading with comprehensive error handling and logging - Add user feedback for template seeding and loading errors - Improve fetchData() and seedTemplates() with try-catch blocks - Add console logging for debugging template initialization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,16 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { Eye, Send, Mail } from 'lucide-react';
|
import { Eye, Send, Mail, X } from 'lucide-react';
|
||||||
import { EmailTemplateRenderer, ShortcodeProcessor } from '@/lib/email-templates/master-template';
|
import { EmailTemplateRenderer, ShortcodeProcessor } from '@/lib/email-templates/master-template';
|
||||||
|
|
||||||
interface NotificationTemplate {
|
interface NotificationTemplate {
|
||||||
@@ -22,9 +30,17 @@ interface EmailTemplatePreviewProps {
|
|||||||
template: NotificationTemplate;
|
template: NotificationTemplate;
|
||||||
onTest?: (template: NotificationTemplate) => void;
|
onTest?: (template: NotificationTemplate) => void;
|
||||||
isTestSending?: boolean;
|
isTestSending?: boolean;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmailTemplatePreview({ template, onTest, isTestSending = false }: EmailTemplatePreviewProps) {
|
export function EmailTemplatePreview({
|
||||||
|
template,
|
||||||
|
onTest,
|
||||||
|
isTestSending = false,
|
||||||
|
open,
|
||||||
|
onClose
|
||||||
|
}: EmailTemplatePreviewProps) {
|
||||||
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master');
|
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master');
|
||||||
const [testEmail, setTestEmail] = useState('');
|
const [testEmail, setTestEmail] = useState('');
|
||||||
const [showTestForm, setShowTestForm] = useState(false);
|
const [showTestForm, setShowTestForm] = useState(false);
|
||||||
@@ -60,6 +76,25 @@ export function EmailTemplatePreview({ template, onTest, isTestSending = false }
|
|||||||
const previewHtml = generatePreview();
|
const previewHtml = generatePreview();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Mail className="w-5 h-5" />
|
||||||
|
Preview: {template.name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Preview template email dengan master styling
|
||||||
|
</DialogDescription>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Preview Controls */}
|
{/* Preview Controls */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -134,7 +169,7 @@ export function EmailTemplatePreview({ template, onTest, isTestSending = false }
|
|||||||
{previewMode === 'master' ? 'Full Email Preview' : 'Content Preview'}
|
{previewMode === 'master' ? 'Full Email Preview' : 'Content Preview'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white" style={{ height: '600px', overflow: 'hidden' }}>
|
<div className="bg-white" style={{ height: '500px', overflow: 'hidden' }}>
|
||||||
<iframe
|
<iframe
|
||||||
srcDoc={previewHtml}
|
srcDoc={previewHtml}
|
||||||
className="w-full h-full border-0"
|
className="w-full h-full border-0"
|
||||||
@@ -163,6 +198,32 @@ export function EmailTemplatePreview({ template, onTest, isTestSending = false }
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Template Actions */}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
{!showTestForm && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowTestForm(true)}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
Test Email
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showTestForm && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowTestForm(false)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
</div>
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -346,26 +346,55 @@ export function NotifikasiTab() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set());
|
const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set());
|
||||||
const [testingTemplate, setTestingTemplate] = useState<string | null>(null);
|
const [testingTemplate, setTestingTemplate] = useState<string | null>(null);
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
|
const [previewTemplate, setPreviewTemplate] = useState<NotificationTemplate | null>(null);
|
||||||
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master');
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Fetching templates...');
|
||||||
// Fetch templates
|
// Fetch templates
|
||||||
const { data: templatesData } = await supabase.from('notification_templates').select('*').order('key');
|
const { data: templatesData, error: fetchError } = await supabase.from('notification_templates').select('*').order('key');
|
||||||
|
|
||||||
|
if (fetchError) {
|
||||||
|
console.error('Error fetching templates:', fetchError);
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Gagal mengambil template: ' + fetchError.message,
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Templates data:', templatesData);
|
||||||
|
|
||||||
if (templatesData && templatesData.length > 0) {
|
if (templatesData && templatesData.length > 0) {
|
||||||
|
console.log('Setting templates from database:', templatesData.length);
|
||||||
setTemplates(templatesData);
|
setTemplates(templatesData);
|
||||||
} else {
|
} else {
|
||||||
|
console.log('No templates found, seeding default templates...');
|
||||||
// Seed default templates if none exist
|
// Seed default templates if none exist
|
||||||
await seedTemplates();
|
await seedTemplates();
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unexpected error in fetchData:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Terjadi kesalahan tak terduga saat mengambil data',
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const seedTemplates = async () => {
|
const seedTemplates = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Seeding default templates...');
|
||||||
const toInsert = DEFAULT_TEMPLATES.map(t => ({
|
const toInsert = DEFAULT_TEMPLATES.map(t => ({
|
||||||
key: t.key,
|
key: t.key,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
@@ -374,8 +403,36 @@ export function NotifikasiTab() {
|
|||||||
email_body_html: t.defaultBody,
|
email_body_html: t.defaultBody,
|
||||||
webhook_url: '',
|
webhook_url: '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log('Inserting templates:', toInsert.length);
|
||||||
const { data, error } = await supabase.from('notification_templates').insert(toInsert).select();
|
const { data, error } = await supabase.from('notification_templates').insert(toInsert).select();
|
||||||
if (!error && data) setTemplates(data);
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error seeding templates:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Gagal membuat template default: ' + error.message,
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Templates seeded successfully:', data);
|
||||||
|
if (data) {
|
||||||
|
setTemplates(data);
|
||||||
|
toast({
|
||||||
|
title: 'Berhasil',
|
||||||
|
description: `Berhasil membuat ${data.length} template default`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unexpected error in seedTemplates:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Terjadi kesalahan saat membuat template default',
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -587,7 +644,8 @@ export function NotifikasiTab() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateTemplate(template);
|
updateTemplate(template);
|
||||||
setSelectedTemplate(template);
|
setPreviewTemplate(template);
|
||||||
|
setIsPreviewOpen(true);
|
||||||
}}
|
}}
|
||||||
className="shadow-sm flex-1"
|
className="shadow-sm flex-1"
|
||||||
>
|
>
|
||||||
@@ -620,33 +678,14 @@ export function NotifikasiTab() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Consolidated Email Preview */}
|
{/* Modal Email Preview */}
|
||||||
{selectedTemplate && (
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center justify-between">
|
|
||||||
<span>Preview: {selectedTemplate.name}</span>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedTemplate(null)}
|
|
||||||
>
|
|
||||||
Tutup Preview
|
|
||||||
</Button>
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Preview template email dengan master styling
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<EmailTemplatePreview
|
<EmailTemplatePreview
|
||||||
template={selectedTemplate}
|
template={previewTemplate!}
|
||||||
|
open={isPreviewOpen}
|
||||||
|
onClose={() => setIsPreviewOpen(false)}
|
||||||
onTest={sendTestEmail}
|
onTest={sendTestEmail}
|
||||||
isTestSending={testingTemplate === selectedTemplate.id}
|
isTestSending={testingTemplate === previewTemplate?.id}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user