mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
fix: templates in whatsapp (#9862)
This commit is contained in:
@@ -30,7 +30,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<woot-button variant="smooth" @click="$emit('resetTemplate')">
|
<woot-button variant="smooth" @click="resetTemplate">
|
||||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button type="button" @click="sendMessage">
|
<woot-button type="button" @click="sendMessage">
|
||||||
@@ -41,82 +41,108 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
/**
|
||||||
|
* This component handles parsing and sending WhatsApp message templates.
|
||||||
|
* It works as follows:
|
||||||
|
* 1. Displays the template text with variable placeholders.
|
||||||
|
* 2. Generates input fields for each variable in the template.
|
||||||
|
* 3. Validates that all variables are filled before sending.
|
||||||
|
* 4. Replaces placeholders with user-provided values.
|
||||||
|
* 5. Emits events to send the processed message or reset the template.
|
||||||
|
*/
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { requiredIf } from '@vuelidate/validators';
|
import { requiredIf } from '@vuelidate/validators';
|
||||||
const allKeysRequired = value => {
|
|
||||||
const keys = Object.keys(value);
|
|
||||||
return keys.every(key => value[key]);
|
|
||||||
};
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
template: {
|
template: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup(props, { emit }) {
|
||||||
return { v$: useVuelidate() };
|
const processVariable = str => {
|
||||||
},
|
return str.replace(/{{|}}/g, '');
|
||||||
validations: {
|
|
||||||
processedParams: {
|
|
||||||
requiredIfKeysPresent: requiredIf('variables'),
|
|
||||||
allKeysRequired,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
processedParams: {},
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
computed: {
|
const allKeysRequired = value => {
|
||||||
variables() {
|
const keys = Object.keys(value);
|
||||||
const variables = this.templateString.match(/{{([^}]+)}}/g);
|
return keys.every(key => value[key]);
|
||||||
return variables;
|
};
|
||||||
},
|
|
||||||
templateString() {
|
const processedParams = ref({});
|
||||||
return this.template.components.find(
|
|
||||||
|
const templateString = computed(() => {
|
||||||
|
return props.template.components.find(
|
||||||
component => component.type === 'BODY'
|
component => component.type === 'BODY'
|
||||||
).text;
|
).text;
|
||||||
},
|
});
|
||||||
processedString() {
|
|
||||||
return this.templateString.replace(/{{([^}]+)}}/g, (match, variable) => {
|
const variables = computed(() => {
|
||||||
const variableKey = this.processVariable(variable);
|
return templateString.value.match(/{{([^}]+)}}/g);
|
||||||
return this.processedParams[variableKey] || `{{${variable}}}`;
|
});
|
||||||
|
|
||||||
|
const processedString = computed(() => {
|
||||||
|
return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||||
|
const variableKey = processVariable(variable);
|
||||||
|
return processedParams.value[variableKey] || `{{${variable}}}`;
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
},
|
|
||||||
mounted() {
|
const v$ = useVuelidate(
|
||||||
this.generateVariables();
|
{
|
||||||
},
|
processedParams: {
|
||||||
methods: {
|
requiredIfKeysPresent: requiredIf(variables),
|
||||||
sendMessage() {
|
allKeysRequired,
|
||||||
this.v$.$touch();
|
|
||||||
if (this.v$.$invalid) return;
|
|
||||||
const payload = {
|
|
||||||
message: this.processedString,
|
|
||||||
templateParams: {
|
|
||||||
name: this.template.name,
|
|
||||||
category: this.template.category,
|
|
||||||
language: this.template.language,
|
|
||||||
namespace: this.template.namespace,
|
|
||||||
processed_params: this.processedParams,
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
this.$emit('sendMessage', payload);
|
{ processedParams }
|
||||||
},
|
);
|
||||||
processVariable(str) {
|
|
||||||
return str.replace(/{{|}}/g, '');
|
const generateVariables = () => {
|
||||||
},
|
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||||
generateVariables() {
|
|
||||||
const matchedVariables = this.templateString.match(/{{([^}]+)}}/g);
|
|
||||||
if (!matchedVariables) return;
|
if (!matchedVariables) return;
|
||||||
|
|
||||||
const variables = matchedVariables.map(i => this.processVariable(i));
|
const finalVars = matchedVariables.map(i => processVariable(i));
|
||||||
this.processedParams = variables.reduce((acc, variable) => {
|
processedParams.value = finalVars.reduce((acc, variable) => {
|
||||||
acc[variable] = '';
|
acc[variable] = '';
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
},
|
};
|
||||||
|
|
||||||
|
const resetTemplate = () => {
|
||||||
|
emit('resetTemplate');
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
v$.value.$touch();
|
||||||
|
if (v$.value.$invalid) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message: processedString.value,
|
||||||
|
templateParams: {
|
||||||
|
name: props.template.name,
|
||||||
|
category: props.template.category,
|
||||||
|
language: props.template.language,
|
||||||
|
namespace: props.template.namespace,
|
||||||
|
processed_params: processedParams.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
emit('sendMessage', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(generateVariables);
|
||||||
|
|
||||||
|
return {
|
||||||
|
processedParams,
|
||||||
|
variables,
|
||||||
|
templateString,
|
||||||
|
processedString,
|
||||||
|
v$,
|
||||||
|
resetTemplate,
|
||||||
|
sendMessage,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -285,10 +285,6 @@ export default {
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
channelType: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
|
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { templates } from './fixtures';
|
|||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
import VueI18n from 'vue-i18n';
|
import VueI18n from 'vue-i18n';
|
||||||
import i18n from 'dashboard/i18n';
|
import i18n from 'dashboard/i18n';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
localVue.use(VueI18n);
|
localVue.use(VueI18n);
|
||||||
|
|
||||||
@@ -18,30 +19,32 @@ const config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('#WhatsAppTemplates', () => {
|
describe('#WhatsAppTemplates', () => {
|
||||||
it('returns all variables from a template string', () => {
|
it('returns all variables from a template string', async () => {
|
||||||
const wrapper = shallowMount(TemplateParser, {
|
const wrapper = shallowMount(TemplateParser, {
|
||||||
...config,
|
...config,
|
||||||
propsData: { template: templates[0] },
|
propsData: { template: templates[0] },
|
||||||
});
|
});
|
||||||
|
await nextTick();
|
||||||
expect(wrapper.vm.variables).toEqual(['{{1}}', '{{2}}', '{{3}}']);
|
expect(wrapper.vm.variables).toEqual(['{{1}}', '{{2}}', '{{3}}']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns no variables from a template string if it does not contain variables', () => {
|
it('returns no variables from a template string if it does not contain variables', async () => {
|
||||||
const wrapper = shallowMount(TemplateParser, {
|
const wrapper = shallowMount(TemplateParser, {
|
||||||
...config,
|
...config,
|
||||||
propsData: { template: templates[12] },
|
propsData: { template: templates[12] },
|
||||||
});
|
});
|
||||||
|
await nextTick();
|
||||||
expect(wrapper.vm.variables).toBeNull();
|
expect(wrapper.vm.variables).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the body of a template', () => {
|
it('returns the body of a template', async () => {
|
||||||
const wrapper = shallowMount(TemplateParser, {
|
const wrapper = shallowMount(TemplateParser, {
|
||||||
...config,
|
...config,
|
||||||
propsData: { template: templates[1] },
|
propsData: { template: templates[1] },
|
||||||
});
|
});
|
||||||
const expectedOutput = templates[1].components.find(
|
await nextTick();
|
||||||
i => i.type === 'BODY'
|
const expectedOutput =
|
||||||
).text;
|
templates[1].components.find(i => i.type === 'BODY')?.text || '';
|
||||||
expect(wrapper.vm.templateString).toEqual(expectedOutput);
|
expect(wrapper.vm.templateString).toEqual(expectedOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,14 +52,12 @@ describe('#WhatsAppTemplates', () => {
|
|||||||
const wrapper = shallowMount(TemplateParser, {
|
const wrapper = shallowMount(TemplateParser, {
|
||||||
...config,
|
...config,
|
||||||
propsData: { template: templates[0] },
|
propsData: { template: templates[0] },
|
||||||
data: () => {
|
|
||||||
return { processedParams: {} };
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
await nextTick();
|
||||||
await wrapper.setData({
|
await wrapper.setData({
|
||||||
processedParams: { 1: 'abc', 2: 'xyz', 3: 'qwerty' },
|
processedParams: { 1: 'abc', 2: 'xyz', 3: 'qwerty' },
|
||||||
});
|
});
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
const expectedOutput =
|
const expectedOutput =
|
||||||
'Esta é a sua confirmação de voo para abc-xyz em qwerty.';
|
'Esta é a sua confirmação de voo para abc-xyz em qwerty.';
|
||||||
expect(wrapper.vm.processedString).toEqual(expectedOutput);
|
expect(wrapper.vm.processedString).toEqual(expectedOutput);
|
||||||
|
|||||||
Reference in New Issue
Block a user