feat(scheduled-messages): add WhatsApp templates (#205)

* feat(scheduled-messages): add WhatsApp templates

* fix: update inboxId prop type to accept both Number and String; localize template labels in conversation.json
This commit is contained in:
Gabriel Jablonski 2026-02-02 19:50:17 -03:00 committed by GitHub
parent e84875ca04
commit da466c88a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 138 additions and 11 deletions

View File

@ -109,6 +109,8 @@ const templateName = computed(() => {
return templateParams.name || templateParams.id; return templateParams.name || templateParams.id;
}); });
const hasTemplate = computed(() => Boolean(templateName.value));
const attachment = computed(() => props.scheduledMessage?.attachment); const attachment = computed(() => props.scheduledMessage?.attachment);
const attachmentName = computed(() => attachment.value?.filename); const attachmentName = computed(() => attachment.value?.filename);
const attachmentUrl = computed(() => attachment.value?.file_url); const attachmentUrl = computed(() => attachment.value?.file_url);
@ -247,6 +249,20 @@ watch(previewContent, () => {
</Button> </Button>
</div> </div>
<div
v-if="hasTemplate"
class="flex items-center gap-1.5 text-xs text-n-slate-11"
>
<Icon icon="i-lucide-zap" class="size-3 shrink-0" />
<span class="truncate">
{{
t('SCHEDULED_MESSAGES.ITEM.TEMPLATE_LABEL', {
name: templateName,
})
}}
</span>
</div>
<div <div
v-if="shouldShowAttachmentLine" v-if="shouldShowAttachmentLine"
class="flex items-center gap-1.5 text-xs text-n-slate-11" class="flex items-center gap-1.5 text-xs text-n-slate-11"

View File

@ -35,6 +35,10 @@ const props = defineProps({
return true; return true;
}, },
}, },
sendButtonLabel: {
type: String,
default: '',
},
}); });
const emit = defineEmits(['sendMessage', 'resetTemplate', 'back']); const emit = defineEmits(['sendMessage', 'resetTemplate', 'back']);
@ -43,6 +47,12 @@ const { t } = useI18n();
const processedParams = ref({}); const processedParams = ref({});
const sendButtonText = computed(() => {
return (
props.sendButtonLabel || t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')
);
});
const languageLabel = computed(() => { const languageLabel = computed(() => {
return `${t('WHATSAPP_TEMPLATES.PARSER.LANGUAGE')}: ${props.template.language || DEFAULT_LANGUAGE}`; return `${t('WHATSAPP_TEMPLATES.PARSER.LANGUAGE')}: ${props.template.language || DEFAULT_LANGUAGE}`;
}); });
@ -305,6 +315,7 @@ defineExpose({
:go-back="goBack" :go-back="goBack"
:is-valid="!v$.$invalid" :is-valid="!v$.$invalid"
:disabled="isFormInvalid" :disabled="isFormInvalid"
:send-button-text="sendButtonText"
/> />
</div> </div>
</template> </template>

View File

@ -12,9 +12,13 @@ export default {
default: false, default: false,
}, },
inboxId: { inboxId: {
type: Number, type: [Number, String],
default: undefined, default: undefined,
}, },
sendButtonLabel: {
type: String,
default: '',
},
}, },
emits: ['onSend', 'cancel', 'update:show'], emits: ['onSend', 'cancel', 'update:show'],
data() { data() {
@ -39,6 +43,13 @@ export default {
: this.$t('WHATSAPP_TEMPLATES.MODAL.SUBTITLE'); : this.$t('WHATSAPP_TEMPLATES.MODAL.SUBTITLE');
}, },
}, },
watch: {
show(newVal) {
if (newVal) {
this.selectedWaTemplate = null;
}
},
},
methods: { methods: {
pickTemplate(template) { pickTemplate(template) {
this.selectedWaTemplate = template; this.selectedWaTemplate = template;
@ -71,6 +82,7 @@ export default {
<WhatsAppTemplateReply <WhatsAppTemplateReply
v-else v-else
:template="selectedWaTemplate" :template="selectedWaTemplate"
:send-button-label="sendButtonLabel"
@reset-template="onResetTemplate" @reset-template="onResetTemplate"
@send-message="onSendMessage" @send-message="onSendMessage"
/> />

View File

@ -2,11 +2,15 @@
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue'; import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
defineProps({ const props = defineProps({
template: { template: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
sendButtonLabel: {
type: String,
default: '',
},
}); });
const emit = defineEmits(['sendMessage', 'resetTemplate']); const emit = defineEmits(['sendMessage', 'resetTemplate']);
@ -23,11 +27,14 @@ const handleResetTemplate = () => {
<template> <template>
<div class="w-full"> <div class="w-full">
<WhatsAppTemplateParser <WhatsAppTemplateParser
:template="template" :template="props.template"
:send-button-label="props.sendButtonLabel"
@send-message="handleSendMessage" @send-message="handleSendMessage"
@reset-template="handleResetTemplate" @reset-template="handleResetTemplate"
> >
<template #actions="{ sendMessage, resetTemplate, disabled }"> <template
#actions="{ sendMessage, resetTemplate, disabled, sendButtonText }"
>
<footer class="flex gap-2 justify-end"> <footer class="flex gap-2 justify-end">
<NextButton <NextButton
faded faded
@ -38,7 +45,7 @@ const handleResetTemplate = () => {
/> />
<NextButton <NextButton
type="button" type="button"
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')" :label="sendButtonText"
:disabled="disabled" :disabled="disabled"
@click="sendMessage" @click="sendMessage"
/> />

View File

@ -410,6 +410,7 @@
"SCHEDULED_FOR": "Scheduled for {time}", "SCHEDULED_FOR": "Scheduled for {time}",
"NO_SCHEDULE": "No schedule", "NO_SCHEDULE": "No schedule",
"TEMPLATE_PREVIEW": "Template: {name}", "TEMPLATE_PREVIEW": "Template: {name}",
"TEMPLATE_LABEL": "Template: {name}",
"ATTACHMENT_PREVIEW": "Attachment: {filename}", "ATTACHMENT_PREVIEW": "Attachment: {filename}",
"ATTACHMENT_LABEL": "Attachment: {filename}", "ATTACHMENT_LABEL": "Attachment: {filename}",
"EMPTY_PREVIEW": "No content", "EMPTY_PREVIEW": "No content",
@ -426,6 +427,9 @@
"ATTACHMENT_LABEL": "Attachment", "ATTACHMENT_LABEL": "Attachment",
"ATTACHMENT_ADD": "Attach file", "ATTACHMENT_ADD": "Attach file",
"ATTACHMENT_CURRENT": "Current attachment: {filename}", "ATTACHMENT_CURRENT": "Current attachment: {filename}",
"TEMPLATE_SELECT": "Use template",
"TEMPLATE_SELECTED": "Template: {name}",
"TEMPLATE_ACTION": "Schedule message",
"CANCEL": "Cancel", "CANCEL": "Cancel",
"SAVE_DRAFT": "Save as draft", "SAVE_DRAFT": "Save as draft",
"SCHEDULE": "Schedule" "SCHEDULE": "Schedule"

View File

@ -408,7 +408,8 @@
"ITEM": { "ITEM": {
"SCHEDULED_FOR": "Agendada para {time}", "SCHEDULED_FOR": "Agendada para {time}",
"NO_SCHEDULE": "Sem agendamento", "NO_SCHEDULE": "Sem agendamento",
"TEMPLATE_PREVIEW": "Template: {name}", "TEMPLATE_PREVIEW": "Modelo: {name}",
"TEMPLATE_LABEL": "Modelo: {name}",
"ATTACHMENT_PREVIEW": "Anexo: {filename}", "ATTACHMENT_PREVIEW": "Anexo: {filename}",
"ATTACHMENT_LABEL": "Anexo: {filename}", "ATTACHMENT_LABEL": "Anexo: {filename}",
"EMPTY_PREVIEW": "Sem conteúdo", "EMPTY_PREVIEW": "Sem conteúdo",
@ -425,6 +426,9 @@
"ATTACHMENT_LABEL": "Anexo", "ATTACHMENT_LABEL": "Anexo",
"ATTACHMENT_ADD": "Anexar arquivo", "ATTACHMENT_ADD": "Anexar arquivo",
"ATTACHMENT_CURRENT": "Anexo atual: {filename}", "ATTACHMENT_CURRENT": "Anexo atual: {filename}",
"TEMPLATE_SELECT": "Usar modelo",
"TEMPLATE_SELECTED": "Modelo: {name}",
"TEMPLATE_ACTION": "Agendar mensagem",
"CANCEL": "Cancelar", "CANCEL": "Cancelar",
"SAVE_DRAFT": "Salvar como rascunho", "SAVE_DRAFT": "Salvar como rascunho",
"SCHEDULE": "Agendar" "SCHEDULE": "Agendar"

View File

@ -17,6 +17,7 @@ import DropdownContainer from 'next/dropdown-menu/base/DropdownContainer.vue';
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue'; import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue'; import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue'; import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
import WhatsappTemplates from 'dashboard/components/widgets/conversation/WhatsappTemplates/Modal.vue';
const props = defineProps({ const props = defineProps({
show: { show: {
@ -59,12 +60,21 @@ const isUpdating = computed(() => uiFlags.value.isUpdating);
const isSubmitting = computed(() => isCreating.value || isUpdating.value); const isSubmitting = computed(() => isCreating.value || isUpdating.value);
const currentInbox = computed(() => inboxGetter.value(props.inboxId)); const currentInbox = computed(() => inboxGetter.value(props.inboxId));
const whatsAppTemplates = computed(() => {
return store.getters['inboxes/getWhatsAppTemplates'](props.inboxId) || [];
});
const showWhatsappTemplates = computed(() => {
return whatsAppTemplates.value.length > 0;
});
const messageContent = ref(''); const messageContent = ref('');
const scheduledDateTime = ref(null); const scheduledDateTime = ref(null);
const attachments = ref([]); const attachments = ref([]);
const existingAttachment = ref(null); const existingAttachment = ref(null);
const templateParams = ref(null); const templateParams = ref(null);
const showConfirmClose = ref(false); const showConfirmClose = ref(false);
const showWhatsAppTemplatesModal = ref(false);
const datePickerOpen = ref(false); const datePickerOpen = ref(false);
const contentError = ref(false); const contentError = ref(false);
const contentLengthError = ref(false); const contentLengthError = ref(false);
@ -172,7 +182,18 @@ const hasTemplate = computed(
() => templateParams.value && Object.keys(templateParams.value).length () => templateParams.value && Object.keys(templateParams.value).length
); );
const hasExistingAttachment = computed(() => !!existingAttachment.value); const hasExistingAttachment = computed(() => !!existingAttachment.value);
const showAttachmentUpload = computed(() => !hasNewAttachment.value); const showAttachmentUpload = computed(
() => !hasNewAttachment.value && !hasTemplate.value
);
const templateName = computed(() => {
return templateParams.value?.name || templateParams.value?.id || null;
});
const clearTemplate = () => {
templateParams.value = null;
messageContent.value = '';
};
const maxLength = computed(() => { const maxLength = computed(() => {
const channelType = currentInbox.value?.channel_type; const channelType = currentInbox.value?.channel_type;
@ -354,6 +375,21 @@ const closeModal = () => {
resetForm(); resetForm();
}; };
const openWhatsAppTemplatesModal = () => {
showWhatsAppTemplatesModal.value = true;
};
const hideWhatsAppTemplatesModal = () => {
showWhatsAppTemplatesModal.value = false;
};
const onTemplateSelect = messagePayload => {
templateParams.value = messagePayload.templateParams;
messageContent.value = messagePayload.message;
hideWhatsAppTemplatesModal();
contentError.value = false;
};
const submit = async status => { const submit = async status => {
if (!validatePayload(status)) return; if (!validatePayload(status)) return;
@ -458,14 +494,16 @@ watch(
<WootMessageEditor <WootMessageEditor
v-model="messageContent" v-model="messageContent"
class="message-editor min-h-[10rem] max-h-[20rem] !px-3 resize-y overflow-auto" class="message-editor min-h-[10rem] max-h-[20rem] !px-3 resize-y overflow-auto"
:class=" :class="[
contentError || contentLengthError contentError || contentLengthError
? 'border border-n-ruby-9 rounded-xl' ? 'border border-n-ruby-9 rounded-xl'
: '' : '',
" hasTemplate ? 'opacity-60 cursor-not-allowed' : '',
]"
:placeholder="t('SCHEDULED_MESSAGES.MODAL.MESSAGE_PLACEHOLDER')" :placeholder="t('SCHEDULED_MESSAGES.MODAL.MESSAGE_PLACEHOLDER')"
:channel-type="currentInbox?.channel_type" :channel-type="currentInbox?.channel_type"
:medium="currentInbox?.medium" :medium="currentInbox?.medium"
:disabled="hasTemplate"
override-line-breaks override-line-breaks
@update:model-value=" @update:model-value="
() => { () => {
@ -518,7 +556,7 @@ watch(
@change="onDateTimeChange" @change="onDateTimeChange"
/> />
</div> </div>
<div v-if="showAttachmentUpload" class="flex items-center h-10"> <div v-if="showAttachmentUpload" class="flex items-center gap-2 h-10">
<FileUpload <FileUpload
:accept="ALLOWED_FILE_TYPES" :accept="ALLOWED_FILE_TYPES"
:multiple="false" :multiple="false"
@ -534,6 +572,33 @@ watch(
class="pointer-events-none" class="pointer-events-none"
/> />
</FileUpload> </FileUpload>
<NextButton
v-if="showWhatsappTemplates"
ghost
xs
icon="i-lucide-zap"
:label="t('SCHEDULED_MESSAGES.MODAL.TEMPLATE_SELECT')"
@click="openWhatsAppTemplatesModal"
/>
</div>
<div
v-if="hasTemplate"
class="flex items-center gap-2 text-xs text-n-slate-11"
>
<span>
{{
t('SCHEDULED_MESSAGES.MODAL.TEMPLATE_SELECTED', {
name: templateName,
})
}}
</span>
<NextButton
ghost
xs
slate
icon="i-lucide-x"
@click="clearTemplate"
/>
</div> </div>
<span <span
v-if="existingAttachment && !attachments.length" v-if="existingAttachment && !attachments.length"
@ -632,5 +697,13 @@ watch(
</div> </div>
</div> </div>
</woot-modal> </woot-modal>
<WhatsappTemplates
v-model:show="showWhatsAppTemplatesModal"
:inbox-id="inboxId"
:send-button-label="t('SCHEDULED_MESSAGES.MODAL.TEMPLATE_ACTION')"
@on-send="onTemplateSelect"
@cancel="hideWhatsAppTemplatesModal"
/>
</woot-modal> </woot-modal>
</template> </template>