Fix button roundtrip in editor, alignment persistence, and test email rendering

This commit is contained in:
Dwindi Ramadhana
2026-01-17 13:10:50 +07:00
parent 0e9ace902d
commit 6d2136d3b5
61 changed files with 8287 additions and 866 deletions

View File

@@ -10,7 +10,8 @@ import { EmailBuilder, EmailBlock, blocksToMarkdown, markdownToBlocks } from '@/
import { CodeEditor } from '@/components/ui/code-editor';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ArrowLeft, Eye, Edit, RotateCcw, FileText } from 'lucide-react';
import { ArrowLeft, Eye, Edit, RotateCcw, FileText, Send } from 'lucide-react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { toast } from 'sonner';
import { __ } from '@/lib/i18n';
import { markdownToHtml } from '@/lib/markdown-utils';
@@ -38,12 +39,22 @@ export default function EditTemplate() {
const [blocks, setBlocks] = useState<EmailBlock[]>([]); // Visual mode view (derived from markdown)
const [activeTab, setActiveTab] = useState('preview');
// Fetch email customization settings
// Send Test Email state
const [testEmailDialogOpen, setTestEmailDialogOpen] = useState(false);
const [testEmail, setTestEmail] = useState('');
// Fetch email customization settings (for non-color settings like logo, footer, social links)
const { data: emailSettings } = useQuery({
queryKey: ['email-settings'],
queryFn: () => api.get('/notifications/email-settings'),
});
// Fetch appearance settings for unified colors
const { data: appearanceSettings } = useQuery({
queryKey: ['appearance-settings'],
queryFn: () => api.get('/appearance/settings'),
});
// Fetch template
const { data: template, isLoading, error } = useQuery({
queryKey: ['notification-template', eventId, channelId, recipientType],
@@ -114,6 +125,32 @@ export default function EditTemplate() {
}
};
// Send test email mutation
const sendTestMutation = useMutation({
mutationFn: async (email: string) => {
return api.post(`/notifications/templates/${eventId}/${channelId}/send-test`, {
email,
recipient: recipientType,
});
},
onSuccess: (data: any) => {
toast.success(data.message || __('Test email sent successfully'));
setTestEmailDialogOpen(false);
setTestEmail('');
},
onError: (error: any) => {
toast.error(error?.message || __('Failed to send test email'));
},
});
const handleSendTest = () => {
if (!testEmail || !testEmail.includes('@')) {
toast.error(__('Please enter a valid email address'));
return;
}
sendTestMutation.mutate(testEmail);
};
// Visual mode: Update blocks → Markdown (source of truth)
const handleBlocksChange = (newBlocks: EmailBlock[]) => {
setBlocks(newBlocks);
@@ -288,14 +325,15 @@ export default function EditTemplate() {
}
});
// Get email settings for preview
// Get email settings for preview - use UNIFIED appearance settings for colors
const settings = emailSettings || {};
const primaryColor = settings.primary_color || '#7f54b3';
const secondaryColor = settings.secondary_color || '#7f54b3';
const heroGradientStart = settings.hero_gradient_start || '#667eea';
const heroGradientEnd = settings.hero_gradient_end || '#764ba2';
const heroTextColor = settings.hero_text_color || '#ffffff';
const buttonTextColor = settings.button_text_color || '#ffffff';
const appearColors = appearanceSettings?.data?.general?.colors || appearanceSettings?.general?.colors || {};
const primaryColor = appearColors.primary || '#7f54b3';
const secondaryColor = appearColors.secondary || '#7f54b3';
const heroGradientStart = appearColors.gradientStart || '#667eea';
const heroGradientEnd = appearColors.gradientEnd || '#764ba2';
const heroTextColor = '#ffffff'; // Always white on gradient
const buttonTextColor = '#ffffff'; // Always white on primary
const bodyBgColor = settings.body_bg_color || '#f8f8f8';
const socialIconColor = settings.social_icon_color || 'white';
const logoUrl = settings.logo_url || '';
@@ -307,10 +345,11 @@ export default function EditTemplate() {
const processedFooter = footerText.replace('{current_year}', new Date().getFullYear().toString());
// Generate social icons HTML with PNG images
// Get plugin URL from config, with fallback
const pluginUrl =
(window as any).woonoowData?.pluginUrl ||
(window as any).WNW_CONFIG?.pluginUrl ||
'';
'/wp-content/plugins/woonoow/';
const socialIconsHtml = socialLinks.length > 0 ? `
<div style="margin-top: 16px;">
${socialLinks.map((link: any) => `
@@ -414,128 +453,175 @@ export default function EditTemplate() {
}
return (
<SettingsLayout
title={template.event_label || __('Edit Template')}
description={`${template.channel_label || ''} - ${__('Customize the notification template. Use variables like {customer_name} to personalize messages.')}`}
onSave={handleSave}
saveLabel={__('Save Template')}
isLoading={false}
action={
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
// Determine if staff or customer based on event category
const isStaffEvent = template.event_category === 'staff' || eventId?.includes('admin') || eventId?.includes('staff');
const page = isStaffEvent ? 'staff' : 'customer';
navigate(`/settings/notifications/${page}?tab=events`);
}}
className="gap-2"
title={__('Back')}
>
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">{__('Back')}</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={handleReset}
className="gap-2"
title={__('Reset to Default')}
>
<RotateCcw className="h-4 w-4" />
<span className="hidden sm:inline">{__('Reset to Default')}</span>
</Button>
</div>
}
>
<Card>
<CardContent className="pt-6 space-y-6">
{/* Subject */}
<div className="space-y-2">
<Label htmlFor="subject">{__('Subject / Title')}</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder={__('Enter notification subject')}
/>
<p className="text-xs text-muted-foreground">
{channelId === 'email'
? __('Email subject line')
: __('Push notification title')}
</p>
<>
<SettingsLayout
title={template.event_label || __('Edit Template')}
description={`${template.channel_label || ''} - ${__('Customize the notification template. Use variables like {customer_name} to personalize messages.')}`}
onSave={handleSave}
saveLabel={__('Save Template')}
isLoading={false}
action={
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
// Determine if staff or customer based on event category
const isStaffEvent = template.event_category === 'staff' || eventId?.includes('admin') || eventId?.includes('staff');
const page = isStaffEvent ? 'staff' : 'customer';
navigate(`/settings/notifications/${page}?tab=events`);
}}
className="gap-2"
title={__('Back')}
>
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">{__('Back')}</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={handleReset}
className="gap-2"
title={__('Reset to Default')}
>
<RotateCcw className="h-4 w-4" />
<span className="hidden sm:inline">{__('Reset to Default')}</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setTestEmailDialogOpen(true)}
className="gap-2"
title={__('Send Test')}
>
<Send className="h-4 w-4" />
<span className="hidden sm:inline">{__('Send Test')}</span>
</Button>
</div>
{/* Body */}
<div className="space-y-4">
{/* Three-tab system: Preview | Visual | Markdown */}
<div className="flex items-center justify-between">
<Label>{__('Message Body')}</Label>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
<TabsList className="grid grid-cols-3">
<TabsTrigger value="preview" className="flex items-center gap-1 text-xs">
<Eye className="h-3 w-3" />
{__('Preview')}
</TabsTrigger>
<TabsTrigger value="visual" className="flex items-center gap-1 text-xs">
<Edit className="h-3 w-3" />
{__('Visual')}
</TabsTrigger>
<TabsTrigger value="markdown" className="flex items-center gap-1 text-xs">
<FileText className="h-3 w-3" />
{__('Markdown')}
</TabsTrigger>
</TabsList>
</Tabs>
}
>
<Card>
<CardContent className="pt-6 space-y-6">
{/* Subject */}
<div className="space-y-2">
<Label htmlFor="subject">{__('Subject / Title')}</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder={__('Enter notification subject')}
/>
<p className="text-xs text-muted-foreground">
{channelId === 'email'
? __('Email subject line')
: __('Push notification title')}
</p>
</div>
{/* Preview Tab */}
{activeTab === 'preview' && (
<div className="border rounded-md overflow-hidden">
<iframe
srcDoc={generatePreviewHTML()}
className="w-full min-h-[600px] overflow-hidden bg-white"
title={__('Email Preview')}
/>
{/* Body */}
<div className="space-y-4">
{/* Three-tab system: Preview | Visual | Markdown */}
<div className="flex items-center justify-between">
<Label>{__('Message Body')}</Label>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
<TabsList className="grid grid-cols-3">
<TabsTrigger value="preview" className="flex items-center gap-1 text-xs">
<Eye className="h-3 w-3" />
{__('Preview')}
</TabsTrigger>
<TabsTrigger value="visual" className="flex items-center gap-1 text-xs">
<Edit className="h-3 w-3" />
{__('Visual')}
</TabsTrigger>
<TabsTrigger value="markdown" className="flex items-center gap-1 text-xs">
<FileText className="h-3 w-3" />
{__('Markdown')}
</TabsTrigger>
</TabsList>
</Tabs>
</div>
)}
{/* Visual Tab */}
{activeTab === 'visual' && (
<div>
<EmailBuilder
blocks={blocks}
onChange={handleBlocksChange}
variables={variableKeys}
/>
<p className="text-xs text-muted-foreground mt-2">
{__('Build your email visually. Add blocks, edit content, and switch to Preview to see your branding.')}
</p>
</div>
)}
{/* Preview Tab */}
{activeTab === 'preview' && (
<div className="border rounded-md overflow-hidden">
<iframe
srcDoc={generatePreviewHTML()}
className="w-full min-h-[600px] overflow-hidden bg-white"
title={__('Email Preview')}
/>
</div>
)}
{/* Markdown Tab */}
{activeTab === 'markdown' && (
<div className="space-y-2">
<CodeEditor
value={markdownContent}
onChange={handleMarkdownChange}
placeholder={__('Write in Markdown... Easy and mobile-friendly!')}
supportMarkdown={true}
/>
<p className="text-xs text-muted-foreground">
{__('Write in Markdown - easy to type, even on mobile! Use **bold**, ## headings, [card]...[/card], etc.')}
</p>
<p className="text-xs text-muted-foreground">
{__('All changes are automatically synced between Visual and Markdown modes.')}
</p>
</div>
)}
{/* Visual Tab */}
{activeTab === 'visual' && (
<div>
<EmailBuilder
blocks={blocks}
onChange={handleBlocksChange}
variables={variableKeys}
/>
<p className="text-xs text-muted-foreground mt-2">
{__('Build your email visually. Add blocks, edit content, and switch to Preview to see your branding.')}
</p>
</div>
)}
{/* Markdown Tab */}
{activeTab === 'markdown' && (
<div className="space-y-2">
<CodeEditor
value={markdownContent}
onChange={handleMarkdownChange}
placeholder={__('Write in Markdown... Easy and mobile-friendly!')}
supportMarkdown={true}
/>
<p className="text-xs text-muted-foreground">
{__('Write in Markdown - easy to type, even on mobile! Use **bold**, ## headings, [card]...[/card], etc.')}
</p>
<p className="text-xs text-muted-foreground">
{__('All changes are automatically synced between Visual and Markdown modes.')}
</p>
</div>
)}
</div>
</CardContent>
</Card>
</SettingsLayout>
{/* Send Test Email Dialog */}
<Dialog open={testEmailDialogOpen} onOpenChange={setTestEmailDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{__('Send Test Email')}</DialogTitle>
<DialogDescription>
{__('Send a test email with sample data to verify the template looks correct.')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="test-email">{__('Email Address')}</Label>
<Input
id="test-email"
type="email"
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
placeholder="you@example.com"
/>
<p className="text-xs text-muted-foreground">
{__('The subject will be prefixed with [TEST]')}
</p>
</div>
</div>
</CardContent>
</Card>
</SettingsLayout>
<DialogFooter>
<Button variant="outline" onClick={() => setTestEmailDialogOpen(false)}>
{__('Cancel')}
</Button>
<Button onClick={handleSendTest} disabled={sendTestMutation.isPending}>
{sendTestMutation.isPending ? __('Sending...') : __('Send Test')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}