mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 20:48:07 +00:00
## Description This implementation adds support for the `media_name` parameter for WhatsApp document templates, resolving the issue where documents appear as "untitled" when sent via templates. **Problem solved:** Documents sent via WhatsApp templates always appeared as "untitled" because Chatwoot didn't process the `filename` field required by the WhatsApp API. **Solution:** Added support for the `media_name` parameter that maps to the WhatsApp API's `filename` field. ## Type of change - [x] New feature (non-breaking change which adds functionality) - [x] This change requires a documentation update ## How Has This Been Tested? Created and executed **7 comprehensive test scenarios**: 1. ✅ Document without `media_name` (backward compatibility) 2. ✅ Document with valid `media_name` 3. ✅ Document with blank `media_name` 4. ✅ Document with null `media_name` 5. ✅ Image with `media_name` (ignored as expected) 6. ✅ Video with `media_name` (ignored as expected) 7. ✅ Blank URL (returns nil appropriately) **All tests passed** and confirmed **100% backward compatibility**. ## Technical Implementation **Backend Changes:** - `PopulateTemplateParametersService`: Added `media_name` parameter support - `TemplateProcessorService`: Pass `media_name` to parameter builder - `WhatsappCloudService`: Updated documentation with `media_name` example **Frontend Changes:** - `WhatsAppTemplateParser.vue`: Added UI field for document filename input - `templateHelper.js`: Include `media_name` for document templates - `whatsappTemplates.json`: Added translation key for document name placeholder **Key Features:** - 🔄 **100% Backward Compatible** - Existing templates continue working - 📝 **Document Filename Support** - Users can specify custom filenames - 🎯 **Document-Only Feature** - Only affects document media types - ✅ **Comprehensive Testing** - All edge cases covered ## Expected Behavior **Before:** ```ruby # All documents appear as "untitled" { type: 'document', document: { link: 'https://example.com/document.pdf' } } ``` **After:** ```ruby # With media_name - displays custom filename { type: 'document', document: { link: 'https://example.com/document.pdf', filename: 'Invoice_2025.pdf' } } # Without media_name - works as before { type: 'document', document: { link: 'https://example.com/document.pdf' } } ``` ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
97 lines
3.0 KiB
JavaScript
97 lines
3.0 KiB
JavaScript
// Constants
|
|
export const DEFAULT_LANGUAGE = 'en';
|
|
export const DEFAULT_CATEGORY = 'UTILITY';
|
|
export const COMPONENT_TYPES = {
|
|
HEADER: 'HEADER',
|
|
BODY: 'BODY',
|
|
BUTTONS: 'BUTTONS',
|
|
};
|
|
export const MEDIA_FORMATS = ['IMAGE', 'VIDEO', 'DOCUMENT'];
|
|
|
|
export const findComponentByType = (template, type) =>
|
|
template.components?.find(component => component.type === type);
|
|
|
|
export const processVariable = str => {
|
|
return str.replace(/{{|}}/g, '');
|
|
};
|
|
|
|
export const allKeysRequired = value => {
|
|
const keys = Object.keys(value);
|
|
return keys.every(key => value[key]);
|
|
};
|
|
|
|
export const replaceTemplateVariables = (templateText, processedParams) => {
|
|
return templateText.replace(/{{([^}]+)}}/g, (match, variable) => {
|
|
const variableKey = processVariable(variable);
|
|
return processedParams.body?.[variableKey] || `{{${variable}}}`;
|
|
});
|
|
};
|
|
|
|
export const buildTemplateParameters = (template, hasMediaHeaderValue) => {
|
|
const allVariables = {};
|
|
|
|
const bodyComponent = findComponentByType(template, COMPONENT_TYPES.BODY);
|
|
const headerComponent = findComponentByType(template, COMPONENT_TYPES.HEADER);
|
|
|
|
if (!bodyComponent) return allVariables;
|
|
|
|
const templateString = bodyComponent.text;
|
|
|
|
// Process body variables
|
|
const matchedVariables = templateString.match(/{{([^}]+)}}/g);
|
|
if (matchedVariables) {
|
|
allVariables.body = {};
|
|
matchedVariables.forEach(variable => {
|
|
const key = processVariable(variable);
|
|
allVariables.body[key] = '';
|
|
});
|
|
}
|
|
|
|
if (hasMediaHeaderValue) {
|
|
if (!allVariables.header) allVariables.header = {};
|
|
allVariables.header.media_url = '';
|
|
allVariables.header.media_type = headerComponent.format.toLowerCase();
|
|
|
|
// For document templates, include media_name field for filename support
|
|
if (headerComponent.format.toLowerCase() === 'document') {
|
|
allVariables.header.media_name = '';
|
|
}
|
|
}
|
|
|
|
// Process button variables
|
|
const buttonComponents = template.components.filter(
|
|
component => component.type === COMPONENT_TYPES.BUTTONS
|
|
);
|
|
|
|
buttonComponents.forEach(buttonComponent => {
|
|
if (buttonComponent.buttons) {
|
|
buttonComponent.buttons.forEach((button, index) => {
|
|
// Handle URL buttons with variables
|
|
if (button.type === 'URL' && button.url && button.url.includes('{{')) {
|
|
const buttonVars = button.url.match(/{{([^}]+)}}/g) || [];
|
|
if (buttonVars.length > 0) {
|
|
if (!allVariables.buttons) allVariables.buttons = [];
|
|
allVariables.buttons[index] = {
|
|
type: 'url',
|
|
parameter: '',
|
|
url: button.url,
|
|
variables: buttonVars.map(v => processVariable(v)),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Handle copy code buttons
|
|
if (button.type === 'COPY_CODE') {
|
|
if (!allVariables.buttons) allVariables.buttons = [];
|
|
allVariables.buttons[index] = {
|
|
type: 'copy_code',
|
|
parameter: '',
|
|
};
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return allVariables;
|
|
};
|