feat: Complete markdown syntax refinement and variable protection
✅ New cleaner syntax implemented: - [card:type] instead of [card type='type'] - [button:style](url)Text[/button] instead of [button url='...' style='...'] - Standard markdown images:  ✅ Variable protection from markdown parsing: - Variables with underscores (e.g., {order_items_table}) now protected - HTML comment placeholders prevent italic/bold parsing - All variables render correctly in preview ✅ Button rendering fixes: - Buttons work in Visual mode inside cards - Buttons work in Preview mode - Button clicks prevented in visual editor - Proper styling for solid and outline buttons ✅ Backward compatibility: - Old syntax still supported - No breaking changes ✅ Bug fixes: - Fixed order_item_table → order_items_table naming - Fixed button regex to match across newlines - Added button/image parsing to parseMarkdownBasics - Prevented button clicks on .button and .button-outline classes 📚 Documentation: - NEW_MARKDOWN_SYNTAX.md - Complete user guide - MARKDOWN_SYNTAX_AND_VARIABLES.md - Technical analysis
This commit is contained in:
@@ -6,13 +6,14 @@ import { SettingsLayout } from '../components/SettingsLayout';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { EmailBuilder, EmailBlock, blocksToHTML, htmlToBlocks } from '@/components/EmailBuilder';
|
||||
import { EmailBuilder, EmailBlock, blocksToMarkdown, markdownToBlocks } from '@/components/EmailBuilder';
|
||||
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 } from 'lucide-react';
|
||||
import { ArrowLeft, Eye, Edit, RotateCcw, FileText } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { markdownToHtml } from '@/lib/markdown-utils';
|
||||
|
||||
export default function EditTemplate() {
|
||||
// Mobile responsive check
|
||||
@@ -30,13 +31,13 @@ export default function EditTemplate() {
|
||||
|
||||
const eventId = searchParams.get('event');
|
||||
const channelId = searchParams.get('channel');
|
||||
const recipientType = searchParams.get('recipient') || 'customer'; // Default to customer
|
||||
|
||||
const [subject, setSubject] = useState('');
|
||||
const [body, setBody] = useState('');
|
||||
const [blocks, setBlocks] = useState<EmailBlock[]>([]);
|
||||
const [markdownContent, setMarkdownContent] = useState(''); // Source of truth: Markdown
|
||||
const [blocks, setBlocks] = useState<EmailBlock[]>([]); // Visual mode view (derived from markdown)
|
||||
const [variables, setVariables] = useState<{ [key: string]: string }>({});
|
||||
const [activeTab, setActiveTab] = useState('editor');
|
||||
const [codeMode, setCodeMode] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('preview');
|
||||
|
||||
// Fetch email customization settings
|
||||
const { data: emailSettings } = useQuery({
|
||||
@@ -46,10 +47,10 @@ export default function EditTemplate() {
|
||||
|
||||
// Fetch template
|
||||
const { data: template, isLoading, error } = useQuery({
|
||||
queryKey: ['notification-template', eventId, channelId],
|
||||
queryKey: ['notification-template', eventId, channelId, recipientType],
|
||||
queryFn: async () => {
|
||||
console.log('Fetching template for:', eventId, channelId);
|
||||
const response = await api.get(`/notifications/templates/${eventId}/${channelId}`);
|
||||
console.log('Fetching template for:', eventId, channelId, recipientType);
|
||||
const response = await api.get(`/notifications/templates/${eventId}/${channelId}?recipient=${recipientType}`);
|
||||
console.log('API Response:', response);
|
||||
console.log('API Response.data:', response.data);
|
||||
console.log('API Response type:', typeof response);
|
||||
@@ -67,60 +68,37 @@ export default function EditTemplate() {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.error('No valid template data found in response');
|
||||
return null;
|
||||
},
|
||||
enabled: !!eventId && !!channelId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Template changed:', template);
|
||||
if (template) {
|
||||
console.log('Template data:', {
|
||||
subject: template.subject,
|
||||
body: template.body,
|
||||
variables: template.variables,
|
||||
event_label: template.event_label,
|
||||
channel_label: template.channel_label
|
||||
});
|
||||
|
||||
setSubject(template.subject || '');
|
||||
setBody(template.body || '');
|
||||
setBlocks(htmlToBlocks(template.body || ''));
|
||||
setVariables(template.variables || {});
|
||||
|
||||
// Always treat body as markdown (source of truth)
|
||||
const markdown = template.body || '';
|
||||
setMarkdownContent(markdown);
|
||||
|
||||
// Convert to blocks for visual mode
|
||||
const initialBlocks = markdownToBlocks(markdown);
|
||||
setBlocks(initialBlocks);
|
||||
}
|
||||
}, [template]);
|
||||
|
||||
// Debug: Log when states change
|
||||
useEffect(() => {
|
||||
console.log('Subject state:', subject);
|
||||
}, [subject]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Body state:', body);
|
||||
}, [body]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Variables state:', variables);
|
||||
}, [variables]);
|
||||
|
||||
const handleSave = async () => {
|
||||
// Convert blocks to HTML before saving
|
||||
const htmlBody = codeMode ? body : blocksToHTML(blocks);
|
||||
|
||||
try {
|
||||
await api.post('/notifications/templates', {
|
||||
eventId,
|
||||
channelId,
|
||||
await api.post(`/notifications/templates/${eventId}/${channelId}`, {
|
||||
subject,
|
||||
body: htmlBody,
|
||||
body: markdownContent, // Save markdown (source of truth)
|
||||
recipient: recipientType,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId, recipientType] });
|
||||
toast.success(__('Template saved successfully'));
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || __('Failed to save template'));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,41 +106,43 @@ export default function EditTemplate() {
|
||||
if (!confirm(__('Are you sure you want to reset this template to default?'))) return;
|
||||
|
||||
try {
|
||||
await api.del(`/notifications/templates/${eventId}/${channelId}`);
|
||||
await api.del(`/notifications/templates/${eventId}/${channelId}?recipient=${recipientType}`);
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId, recipientType] });
|
||||
toast.success(__('Template reset to default'));
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || __('Failed to reset template'));
|
||||
}
|
||||
};
|
||||
|
||||
// Sync blocks to body when switching to code mode
|
||||
const handleCodeModeToggle = () => {
|
||||
if (!codeMode) {
|
||||
// Switching TO code mode: convert blocks to HTML
|
||||
setBody(blocksToHTML(blocks));
|
||||
} else {
|
||||
// Switching FROM code mode: convert HTML to blocks
|
||||
setBlocks(htmlToBlocks(body));
|
||||
}
|
||||
setCodeMode(!codeMode);
|
||||
};
|
||||
|
||||
// Update blocks and sync to body
|
||||
// Visual mode: Update blocks → Markdown (source of truth)
|
||||
const handleBlocksChange = (newBlocks: EmailBlock[]) => {
|
||||
setBlocks(newBlocks);
|
||||
setBody(blocksToHTML(newBlocks));
|
||||
const markdown = blocksToMarkdown(newBlocks);
|
||||
setMarkdownContent(markdown); // Update markdown (source of truth)
|
||||
};
|
||||
|
||||
// Markdown mode: Update markdown → Blocks (for visual sync)
|
||||
const handleMarkdownChange = (newMarkdown: string) => {
|
||||
setMarkdownContent(newMarkdown); // Update source of truth
|
||||
const newBlocks = markdownToBlocks(newMarkdown);
|
||||
setBlocks(newBlocks); // Keep blocks in sync
|
||||
};
|
||||
|
||||
// Get variable keys for the rich text editor
|
||||
const variableKeys = Object.keys(variables);
|
||||
|
||||
// Parse [card] tags for preview
|
||||
// Parse [card] tags and [button] shortcodes for preview
|
||||
const parseCardsForPreview = (content: string) => {
|
||||
const cardRegex = /\[card([^\]]*)\](.*?)\[\/card\]/gs;
|
||||
// Parse card blocks - new [card:type] syntax
|
||||
let parsed = content.replace(/\[card:(\w+)\](.*?)\[\/card\]/gs, (match, type, cardContent) => {
|
||||
const cardClass = `card card-${type}`;
|
||||
const htmlContent = markdownToHtml(cardContent.trim());
|
||||
return `<div class="${cardClass}">${htmlContent}</div>`;
|
||||
});
|
||||
|
||||
return content.replace(cardRegex, (match, attributes, cardContent) => {
|
||||
// Parse card blocks - old [card type="..."] syntax (backward compatibility)
|
||||
parsed = parsed.replace(/\[card([^\]]*)\](.*?)\[\/card\]/gs, (match, attributes, cardContent) => {
|
||||
let cardClass = 'card';
|
||||
const typeMatch = attributes.match(/type=["']([^"']+)["']/);
|
||||
if (typeMatch) {
|
||||
@@ -172,13 +152,30 @@ export default function EditTemplate() {
|
||||
const bgMatch = attributes.match(/bg=["']([^"']+)["']/);
|
||||
const bgStyle = bgMatch ? `background-image: url(${bgMatch[1]}); background-size: cover; background-position: center;` : '';
|
||||
|
||||
return `<div class="${cardClass}" style="${bgStyle}">${cardContent}</div>`;
|
||||
// Convert markdown inside card to HTML
|
||||
const htmlContent = markdownToHtml(cardContent.trim());
|
||||
return `<div class="${cardClass}" style="${bgStyle}">${htmlContent}</div>`;
|
||||
});
|
||||
|
||||
// Parse button shortcodes - new [button:style](url)Text[/button] syntax
|
||||
parsed = parsed.replace(/\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/g, (match, style, url, text) => {
|
||||
const buttonClass = style === 'outline' ? 'button-outline' : 'button';
|
||||
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
|
||||
});
|
||||
|
||||
// Parse button shortcodes - old [button url="..."]Text[/button] syntax (backward compatibility)
|
||||
parsed = parsed.replace(/\[button\s+url=["']([^"']+)["'](?:\s+style=["'](solid|outline)["'])?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => {
|
||||
const buttonClass = style === 'outline' ? 'button-outline' : 'button';
|
||||
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
// Generate preview HTML
|
||||
const generatePreviewHTML = () => {
|
||||
let previewBody = body;
|
||||
// Convert markdown to HTML for preview
|
||||
let previewBody = parseCardsForPreview(markdownContent);
|
||||
|
||||
// Replace store-identity variables with actual data
|
||||
const storeVariables: { [key: string]: string } = {
|
||||
@@ -188,7 +185,8 @@ export default function EditTemplate() {
|
||||
};
|
||||
|
||||
Object.entries(storeVariables).forEach(([key, value]) => {
|
||||
previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
|
||||
const regex = new RegExp(`\\{${key}\\}`, 'g');
|
||||
previewBody = previewBody.replace(regex, value);
|
||||
});
|
||||
|
||||
// Replace dynamic variables with sample data (not just highlighting)
|
||||
@@ -198,6 +196,7 @@ export default function EditTemplate() {
|
||||
order_status: 'Processing',
|
||||
order_date: new Date().toLocaleDateString(),
|
||||
order_url: '#',
|
||||
completion_date: new Date().toLocaleDateString(),
|
||||
order_items_list: `<ul style="list-style: none; padding: 0; margin: 16px 0;">
|
||||
<li style="padding: 12px; background: #f9f9f9; border-radius: 6px; margin-bottom: 8px;">
|
||||
<strong>Premium T-Shirt</strong> × 2<br>
|
||||
@@ -244,12 +243,26 @@ export default function EditTemplate() {
|
||||
payment_url: '#',
|
||||
shipping_method: 'Standard Shipping',
|
||||
tracking_number: 'TRACK123456',
|
||||
tracking_url: '#',
|
||||
shipping_carrier: 'Standard Shipping',
|
||||
refund_amount: '$50.00',
|
||||
billing_address: '123 Main St, City, State 12345',
|
||||
shipping_address: '123 Main St, City, State 12345',
|
||||
transaction_id: 'TXN123456789',
|
||||
payment_date: new Date().toLocaleDateString(),
|
||||
payment_status: 'Completed',
|
||||
review_url: '#',
|
||||
shop_url: '#',
|
||||
my_account_url: '#',
|
||||
payment_retry_url: '#',
|
||||
vip_dashboard_url: '#',
|
||||
vip_free_shipping_threshold: '$50',
|
||||
current_year: new Date().getFullYear().toString(),
|
||||
site_name: 'My WordPress Store',
|
||||
store_name: 'My WordPress Store',
|
||||
store_url: '#',
|
||||
store_email: 'store@example.com',
|
||||
support_email: 'support@example.com',
|
||||
};
|
||||
|
||||
Object.keys(sampleData).forEach((key) => {
|
||||
@@ -287,7 +300,10 @@ export default function EditTemplate() {
|
||||
const processedFooter = footerText.replace('{current_year}', new Date().getFullYear().toString());
|
||||
|
||||
// Generate social icons HTML with PNG images
|
||||
const pluginUrl = (window as any).woonoowData?.pluginUrl || '';
|
||||
const pluginUrl =
|
||||
(window as any).woonoowData?.pluginUrl ||
|
||||
(window as any).WNW_CONFIG?.pluginUrl ||
|
||||
'';
|
||||
const socialIconsHtml = socialLinks.length > 0 ? `
|
||||
<div style="margin-top: 16px;">
|
||||
${socialLinks.map((link: any) => `
|
||||
@@ -308,6 +324,15 @@ export default function EditTemplate() {
|
||||
.header { padding: 32px; text-align: center; background: #f8f8f8; }
|
||||
.card-gutter { padding: 0 16px; }
|
||||
.card { background: #ffffff; border-radius: 8px; margin-bottom: 24px; padding: 32px 40px; }
|
||||
|
||||
/* Mobile responsive */
|
||||
@media only screen and (max-width: 600px) {
|
||||
body { padding: 8px; }
|
||||
.card-gutter { padding: 0 8px; }
|
||||
.card { padding: 20px 16px; }
|
||||
.header { padding: 20px 16px; }
|
||||
.footer { padding: 20px 16px; }
|
||||
}
|
||||
.card-success { background: linear-gradient(135deg, ${heroGradientStart} 0%, ${heroGradientEnd} 100%); color: ${heroTextColor}; }
|
||||
.card-success * { color: ${heroTextColor} !important; }
|
||||
.card-highlight { background: linear-gradient(135deg, ${heroGradientStart} 0%, ${heroGradientEnd} 100%); color: ${heroTextColor}; }
|
||||
@@ -316,6 +341,7 @@ export default function EditTemplate() {
|
||||
.card-hero * { color: ${heroTextColor} !important; }
|
||||
.card-info { background: #f0f7ff; border: 1px solid #0071e3; }
|
||||
.card-warning { background: #fff8e1; border: 1px solid #ff9800; }
|
||||
.card-basic { background: none; border: none; padding: 0; margin: 16px 0; }
|
||||
h1 { font-size: 26px; margin-top: 0; margin-bottom: 16px; color: #333; }
|
||||
h2 { font-size: 18px; margin-top: 0; margin-bottom: 16px; color: #333; }
|
||||
h3 { font-size: 16px; margin-top: 0; margin-bottom: 8px; color: #333; }
|
||||
@@ -438,48 +464,40 @@ export default function EditTemplate() {
|
||||
|
||||
{/* Body */}
|
||||
<div className="space-y-4">
|
||||
{/* Tabs for Editor/Preview */}
|
||||
{/* Three-tab system: Preview | Visual | Markdown */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>{__('Message Body')}</Label>
|
||||
{activeTab === 'editor' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCodeModeToggle}
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
{codeMode ? __('Visual Builder') : __('Code Mode')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Label>{__('Message Body')}</Label>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="editor" className="flex items-center gap-1 text-xs">
|
||||
<Edit className="h-3 w-3" />
|
||||
{__('Editor')}
|
||||
</TabsTrigger>
|
||||
<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>
|
||||
|
||||
{activeTab === 'editor' && codeMode ? (
|
||||
<div className="space-y-2">
|
||||
<CodeEditor
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
placeholder={__('Enter HTML code with [card] tags...')}
|
||||
supportMarkdown={true}
|
||||
{/* 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')}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Edit raw HTML code with [card] syntax, or switch to Markdown mode for easier editing.')}
|
||||
</p>
|
||||
</div>
|
||||
) : activeTab === 'editor' ? (
|
||||
)}
|
||||
|
||||
{/* Visual Tab */}
|
||||
{activeTab === 'visual' && (
|
||||
<div>
|
||||
<EmailBuilder
|
||||
blocks={blocks}
|
||||
@@ -487,18 +505,28 @@ export default function EditTemplate() {
|
||||
variables={variableKeys}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{__('Build your email visually. Add blocks, edit content, and see live preview.')}
|
||||
{__('Build your email visually. Add blocks, edit content, and switch to Preview to see your branding.')}
|
||||
</p>
|
||||
</div>
|
||||
) : activeTab === 'preview' ? (
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<iframe
|
||||
srcDoc={generatePreviewHTML()}
|
||||
className="w-full h-[600px] bg-white"
|
||||
title={__('Email Preview')}
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -28,6 +28,10 @@ export default function NotificationTemplates() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [openAccordion, setOpenAccordion] = useState<string | undefined>();
|
||||
|
||||
// Determine recipient type from current page URL (using hash because of HashRouter)
|
||||
const isStaffPage = window.location.hash.includes('/staff');
|
||||
const recipientType = isStaffPage ? 'staff' : 'customer';
|
||||
|
||||
// Check for query params to open specific accordion
|
||||
useEffect(() => {
|
||||
const eventParam = searchParams.get('event');
|
||||
@@ -55,8 +59,8 @@ export default function NotificationTemplates() {
|
||||
});
|
||||
|
||||
const openEditor = (event: any, channel: any) => {
|
||||
// Navigate to edit template subpage
|
||||
navigate(`/settings/notifications/edit-template?event=${event.id}&channel=${channel.id}`);
|
||||
// Navigate to edit template subpage with recipient type
|
||||
navigate(`/settings/notifications/edit-template?event=${event.id}&channel=${channel.id}&recipient=${recipientType}`);
|
||||
};
|
||||
|
||||
const getChannelIcon = (channelId: string) => {
|
||||
@@ -86,6 +90,15 @@ export default function NotificationTemplates() {
|
||||
...(eventsData?.customers || []),
|
||||
];
|
||||
|
||||
// Filter events by recipient type
|
||||
const filteredEvents = allEvents.filter((event: any) => {
|
||||
// Check both recipients array (from get_events) and recipient_type (from get_all_events)
|
||||
if (event.recipients && Array.isArray(event.recipients)) {
|
||||
return event.recipients.includes(recipientType);
|
||||
}
|
||||
return event.recipient_type === recipientType;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Info Card */}
|
||||
@@ -114,7 +127,7 @@ export default function NotificationTemplates() {
|
||||
>
|
||||
<Accordion type="single" collapsible className="w-full" value={openAccordion} onValueChange={setOpenAccordion}>
|
||||
{channels?.map((channel: NotificationChannel) => {
|
||||
const channelTemplates = allEvents.filter((event: any) => {
|
||||
const channelTemplates = filteredEvents.filter((event: any) => {
|
||||
const templateKey = `${event.id}_${channel.id}`;
|
||||
return templates && templates[templateKey];
|
||||
});
|
||||
@@ -129,7 +142,7 @@ export default function NotificationTemplates() {
|
||||
<span className="font-medium text-left">{channel.label}</span>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{allEvents.length} {__('templates')}
|
||||
{filteredEvents.length} {__('templates')}
|
||||
</Badge>
|
||||
{customCount > 0 && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
@@ -142,7 +155,7 @@ export default function NotificationTemplates() {
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-2 pt-2">
|
||||
{allEvents.map((event: any) => {
|
||||
{filteredEvents.map((event: any) => {
|
||||
const templateKey = `${event.id}_${channel.id}`;
|
||||
const hasCustomTemplate = templates && templates[templateKey];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user