chatwoot/app/javascript/dashboard/services/TemplateNormalizer.js
Muhsin Keloth 517b67dfd6
feat: Add rich template preview for WhatsApp & Twilio Templates (#13206)
This PR introduces a comprehensive, display-only template preview system
for both WhatsApp and Twilio templates rendering of all supported
template types.

This lays the foundation for rich template previews across:
- Template selection modals
- Campaign creation flows
- Message composer
- Message screen.

<img width="1818" height="2058" alt="template-preview"
src="https://github.com/user-attachments/assets/9833b28c-f824-4568-8f74-6da09ac61f62"
/>

---------

Co-authored-by: Sivin Varghese <[email protected]>
Co-authored-by: iamsivin <[email protected]>
Co-authored-by: Muhsin <[email protected]>
2026-04-30 18:54:34 +04:00

115 lines
3.7 KiB
JavaScript

import { TemplateTypeDetector } from './TemplateTypeDetector';
import {
PLATFORMS,
TWILIO_TYPE_PREFIX,
WA_COMPONENT_TYPES,
WA_PARAM_FORMATS,
} from './TemplateConstants';
/**
* TemplateNormalizer - Convert platform-specific formats to unified structure
*/
export class TemplateNormalizer {
static normalizeWhatsApp(template) {
const components = template.components || [];
return {
id: template.id,
name: template.name,
platform: PLATFORMS.WHATSAPP,
type: TemplateTypeDetector.detectWhatsAppType(template),
parameterFormat: template.parameter_format || WA_PARAM_FORMATS.POSITIONAL,
header: components.find(c => c.type === WA_COMPONENT_TYPES.HEADER),
body: components.find(c => c.type === WA_COMPONENT_TYPES.BODY),
footer: components.find(c => c.type === WA_COMPONENT_TYPES.FOOTER),
buttons:
components.find(c => c.type === WA_COMPONENT_TYPES.BUTTONS)?.buttons ||
[],
variables: this.extractWhatsAppVariables(template),
category: template.category,
language: template.language,
originalTemplate: template,
};
}
static normalizeTwilio(template) {
const typeKey =
template.template_type?.replace(TWILIO_TYPE_PREFIX, '') ||
Object.keys(template.types || {})
.map(key => key.replace(TWILIO_TYPE_PREFIX, ''))
.find(key => key);
const hyphenatedKey = `${TWILIO_TYPE_PREFIX}${(typeKey || '').replace(/_/g, '-')}`;
const underscoreKey = `${TWILIO_TYPE_PREFIX}${(typeKey || '').replace(/-/g, '_')}`;
const typeData =
template.types?.[hyphenatedKey] || template.types?.[underscoreKey] || {};
return {
contentSid: template.content_sid,
name: template.friendly_name,
platform: PLATFORMS.TWILIO,
type: TemplateTypeDetector.detectTwilioType(template),
body: template.body || typeData.body,
media: typeData.media || [],
mediaType: template.media_type || null,
actions: typeData.actions || [],
variables: template.variables || {},
category: template.category || 'utility',
language: template.language || 'en',
originalTemplate: template,
};
}
static extractWhatsAppVariables(template) {
const variables = {};
const components = template.components || [];
components.forEach(component => {
if (component.text) {
const matches = component.text.match(/\{\{([^}]+)\}\}/g) || [];
matches.forEach(match => {
const variable = match.replace(/[{}]/g, '');
if (template.parameter_format === WA_PARAM_FORMATS.NAMED) {
const example =
component.example?.body_text_named_params?.find(
p => p.param_name === variable
)?.example || '';
variables[variable] = example;
} else {
const position = parseInt(variable, 10) - 1;
const example = component.example?.body_text?.[0]?.[position] || '';
variables[variable] = example;
}
});
}
if (component.buttons) {
component.buttons.forEach(button => {
if (button.url) {
const matches = button.url.match(/\{\{([^}]+)\}\}/g) || [];
matches.forEach(match => {
const variable = match.replace(/[{}]/g, '');
const example = button.example?.[0] || '';
variables[variable] = example;
});
}
});
}
});
return variables;
}
static normalize(template, platform) {
switch (platform) {
case PLATFORMS.WHATSAPP:
return this.normalizeWhatsApp(template);
case PLATFORMS.TWILIO:
return this.normalizeTwilio(template);
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
}