diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue index 2bb1005b3..b95fda8a4 100644 --- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue @@ -105,7 +105,7 @@ const formErrors = computed(() => ({ })); const hasRequiredTemplateParams = computed(() => { - return templateParserRef.value?.v$?.$invalid === false || true; + return templateParserRef.value?.isFormInvalid === false; }); const isSubmitDisabled = computed( @@ -154,7 +154,7 @@ const prepareCampaignDetails = () => { const handleSubmit = async () => { const isFormValid = await v$.value.$validate(); - if (!isFormValid) return; + if (!isFormValid || !hasRequiredTemplateParams.value) return; emit('submit', prepareCampaignDetails()); resetState(); diff --git a/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue index 620955cb4..12968dd2c 100644 --- a/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue +++ b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue @@ -18,12 +18,12 @@ import Input from 'dashboard/components-next/input/Input.vue'; import { buildTemplateParameters, allKeysRequired, - replaceTemplateVariables, DEFAULT_LANGUAGE, DEFAULT_CATEGORY, COMPONENT_TYPES, MEDIA_FORMATS, findComponentByType, + renderTemplatePreview, } from 'dashboard/helper/templateHelper'; const props = defineProps({ @@ -64,6 +64,12 @@ const bodyText = computed(() => { return bodyComponent.value?.text || ''; }); +const headerText = computed(() => { + return headerComponent.value?.format === 'TEXT' + ? headerComponent.value?.text || '' + : ''; +}); + const hasMediaHeader = computed(() => MEDIA_FORMATS.includes(headerComponent.value?.format) ); @@ -77,12 +83,30 @@ const isDocumentTemplate = computed(() => { return headerComponent.value?.format?.toLowerCase() === 'document'; }); -const hasVariables = computed(() => { +const hasBodyVariables = computed(() => { return bodyText.value?.match(/{{([^}]+)}}/g) !== null; }); +const hasTextHeaderVariables = computed(() => { + return headerText.value?.match(/{{([^}]+)}}/g) !== null; +}); + +const hasVariables = computed( + () => hasBodyVariables.value || hasTextHeaderVariables.value +); + +const renderedHeader = computed(() => { + return renderTemplatePreview( + headerText.value, + processedParams.value.header || {} + ); +}); + const renderedTemplate = computed(() => { - return replaceTemplateVariables(bodyText.value, processedParams.value); + return renderTemplatePreview( + bodyText.value, + processedParams.value.body || {} + ); }); // Completeness validation is shared with the mobile app via @chatwoot/utils. @@ -161,7 +185,9 @@ defineExpose({ hasMediaHeader, isDocumentTemplate, headerComponent, + renderedHeader, renderedTemplate, + isFormInvalid, v$, updateMediaUrl, updateMediaName, @@ -185,6 +211,12 @@ defineExpose({
+ {{ $t('WHATSAPP_TEMPLATES.PARSER.HEADER_VARIABLES_LABEL') }} +
+diff --git a/app/javascript/dashboard/helper/templateHelper.js b/app/javascript/dashboard/helper/templateHelper.js index c875871f4..88fe7948d 100644 --- a/app/javascript/dashboard/helper/templateHelper.js +++ b/app/javascript/dashboard/helper/templateHelper.js @@ -7,6 +7,7 @@ export { COMPONENT_TYPES, findComponentByType, processVariable, + renderTemplatePreview, } from '@chatwoot/utils'; export const DEFAULT_LANGUAGE = 'en'; diff --git a/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json b/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json index cf28312dc..294004db5 100644 --- a/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json +++ b/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json @@ -27,6 +27,7 @@ }, "PARSER": { "VARIABLES_LABEL": "Variables", + "HEADER_VARIABLES_LABEL": "Header variables", "LANGUAGE": "Language", "CATEGORY": "Category", "VARIABLE_PLACEHOLDER": "Enter {variable} value", diff --git a/app/services/whatsapp/template_processor_service.rb b/app/services/whatsapp/template_processor_service.rb index 0aaca2fb1..8f6ab763e 100644 --- a/app/services/whatsapp/template_processor_service.rb +++ b/app/services/whatsapp/template_processor_service.rb @@ -41,7 +41,7 @@ class Whatsapp::TemplateProcessorService processed_params ||= template_params['processed_params'] components = [] - components.concat(process_header_components(processed_params)) + components.concat(process_header_components(processed_params, template)) components.concat(process_body_components(processed_params, template)) components.concat(process_footer_components(processed_params)) components.concat(process_button_components(processed_params)) @@ -49,31 +49,31 @@ class Whatsapp::TemplateProcessorService @template_params = components end - def process_header_components(processed_params) + def process_header_components(processed_params, template) return [] if processed_params['header'].blank? - header_params = build_header_params(processed_params['header']) + header_params = build_header_params(processed_params['header'], template) header_params.present? ? [{ type: 'header', parameters: header_params }] : [] end - def build_header_params(header_data) - header_params = [] - header_data.each do |key, value| - next if value.blank? + def build_header_params(header_data, template) + header_component = template['components']&.find { |component| component['type'] == 'HEADER' } + return build_text_header_params(header_data, template) if header_component&.dig('format') == 'TEXT' - if media_url_with_type?(key, header_data) - media_name = header_data['media_name'] - media_param = parameter_builder.build_media_parameter(value, header_data['media_type'], media_name) - header_params << media_param if media_param - elsif key != 'media_type' && key != 'media_name' - header_params << parameter_builder.build_parameter(value) - end - end - header_params + build_media_header_params(header_data) end - def media_url_with_type?(key, header_data) - key == 'media_url' && header_data['media_type'].present? + def build_text_header_params(header_data, template) + header_data.filter_map do |key, value| + build_text_parameter(key, value, template) if value.present? + end + end + + def build_media_header_params(header_data) + return [] if header_data['media_url'].blank? || header_data['media_type'].blank? + + media_param = parameter_builder.build_media_parameter(header_data['media_url'], header_data['media_type'], header_data['media_name']) + media_param ? [media_param] : [] end def process_body_components(processed_params, template) @@ -82,17 +82,18 @@ class Whatsapp::TemplateProcessorService body_params = processed_params['body'].filter_map do |key, value| next if value.blank? - parameter_format = template['parameter_format'] - if parameter_format == 'NAMED' - parameter_builder.build_named_parameter(key, value) - else - parameter_builder.build_parameter(value) - end + build_text_parameter(key, value, template) end body_params.present? ? [{ type: 'body', parameters: body_params }] : [] end + def build_text_parameter(key, value, template) + return parameter_builder.build_named_parameter(key, value) if template['parameter_format'] == 'NAMED' + + parameter_builder.build_parameter(value) + end + def process_footer_components(processed_params) return [] if processed_params['footer'].blank? diff --git a/package.json b/package.json index 54f1b0350..ddcd1dd6f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", "@chatwoot/prosemirror-schema": "1.4.1", - "@chatwoot/utils": "^0.0.56", + "@chatwoot/utils": "^0.0.57", "@formkit/core": "^1.7.2", "@formkit/vue": "^1.7.2", "@hcaptcha/vue3-hcaptcha": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 443737da9..3718d657b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,8 +28,8 @@ importers: specifier: 1.4.1 version: 1.4.1 '@chatwoot/utils': - specifier: ^0.0.56 - version: 0.0.56 + specifier: ^0.0.57 + version: 0.0.57 '@formkit/core': specifier: ^1.7.2 version: 1.7.2 @@ -455,8 +455,8 @@ packages: '@chatwoot/prosemirror-schema@1.4.1': resolution: {integrity: sha512-skBLslveAW4yYQP0RkJFOJ9lXZbPWE95pLHA+AolsPKw/a1iS/g0SMXHx2gbyHoacrVX6v44+PHMKQYP6L3u0Q==} - '@chatwoot/utils@0.0.56': - resolution: {integrity: sha512-A6dmPLfTSrW4qYNY73btyi4PqpfzcXRSaucscZTQdzNqF6G/QUdgnBmHtho8HeiYby/kSHXaSxLJj+0dx3yEQQ==} + '@chatwoot/utils@0.0.57': + resolution: {integrity: sha512-L4QrCmimkULY9ZQk0MJRLfwry3Pqq2e7psETqoZGYopMZ+8DSWe4t6cModSjTCBnDjqgp/gTl0oorntCYrDE8w==} engines: {node: '>=10'} '@codemirror/commands@6.7.0': @@ -5151,7 +5151,7 @@ snapshots: prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3) prosemirror-view: 1.34.1 - '@chatwoot/utils@0.0.56': + '@chatwoot/utils@0.0.57': dependencies: date-fns: 2.30.0 diff --git a/spec/services/whatsapp/template_processor_service_spec.rb b/spec/services/whatsapp/template_processor_service_spec.rb new file mode 100644 index 000000000..454d323e8 --- /dev/null +++ b/spec/services/whatsapp/template_processor_service_spec.rb @@ -0,0 +1,102 @@ +require 'rails_helper' + +describe Whatsapp::TemplateProcessorService do + subject(:processed_components) do + described_class.new(channel: channel, template_params: template_params).call.last + end + + let(:channel) { instance_double(Channel::Whatsapp, message_templates: [template]) } + let(:template_params) do + { + 'name' => template['name'], + 'language' => template['language'], + 'processed_params' => { 'header' => header_params } + } + end + + context 'with a positional text header' do + let(:template) do + { + 'name' => 'positional_header', + 'language' => 'en_US', + 'status' => 'APPROVED', + 'parameter_format' => 'POSITIONAL', + 'components' => [{ 'type' => 'HEADER', 'format' => 'TEXT', 'text' => 'Welcome {{1}}' }] + } + end + let(:header_params) { { '1' => 'Jane' } } + + it 'builds a positional text parameter' do + expect(processed_components).to eq([ + { + type: 'header', + parameters: [{ type: 'text', text: 'Jane' }] + } + ]) + end + end + + context 'with a named text header' do + let(:template) do + { + 'name' => 'named_header', + 'language' => 'en_US', + 'status' => 'APPROVED', + 'parameter_format' => 'NAMED', + 'components' => [{ 'type' => 'HEADER', 'format' => 'TEXT', 'text' => "Welcome {{#{parameter_name}}}" }] + } + end + let(:header_params) { { parameter_name => 'Jane' } } + + %w[customer_name media_type media_name].each do |name| + context "when the parameter is #{name}" do + let(:parameter_name) { name } + + it 'preserves the parameter name' do + expect(processed_components).to eq([ + { + type: 'header', + parameters: [{ type: 'text', parameter_name: parameter_name, text: 'Jane' }] + } + ]) + end + end + end + end + + context 'with a media header' do + let(:template) do + { + 'name' => 'document_header', + 'language' => 'en_US', + 'status' => 'APPROVED', + 'parameter_format' => 'POSITIONAL', + 'components' => [{ 'type' => 'HEADER', 'format' => 'DOCUMENT' }] + } + end + let(:header_params) do + { + 'media_url' => 'https://example.com/report.pdf', + 'media_type' => 'document', + 'media_name' => 'report.pdf' + } + end + + it 'uses media metadata to build the attachment parameter' do + expect(processed_components).to eq([ + { + type: 'header', + parameters: [ + { + type: 'document', + document: { + link: 'https://example.com/report.pdf', + filename: 'report.pdf' + } + } + ] + } + ]) + end + end +end