Fix button roundtrip in editor, alignment persistence, and test email rendering
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user