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;
});
const hasTemplate = computed(() => Boolean(templateName.value));
const attachment = computed(() => props.scheduledMessage?.attachment);
const attachmentName = computed(() => attachment.value?.filename);
const attachmentUrl = computed(() => attachment.value?.file_url);
@ -247,6 +249,20 @@ watch(previewContent, () => {
</Button>
</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
v-if="shouldShowAttachmentLine"
class="flex items-center gap-1.5 text-xs text-n-slate-11"

View File

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

View File

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

View File

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

View File

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

View File

@ -408,7 +408,8 @@
"ITEM": {
"SCHEDULED_FOR": "Agendada para {time}",
"NO_SCHEDULE": "Sem agendamento",
"TEMPLATE_PREVIEW": "Template: {name}",
"TEMPLATE_PREVIEW": "Modelo: {name}",
"TEMPLATE_LABEL": "Modelo: {name}",
"ATTACHMENT_PREVIEW": "Anexo: {filename}",
"ATTACHMENT_LABEL": "Anexo: {filename}",
"EMPTY_PREVIEW": "Sem conteúdo",
@ -425,6 +426,9 @@
"ATTACHMENT_LABEL": "Anexo",
"ATTACHMENT_ADD": "Anexar arquivo",
"ATTACHMENT_CURRENT": "Anexo atual: {filename}",
"TEMPLATE_SELECT": "Usar modelo",
"TEMPLATE_SELECTED": "Modelo: {name}",
"TEMPLATE_ACTION": "Agendar mensagem",
"CANCEL": "Cancelar",
"SAVE_DRAFT": "Salvar como rascunho",
"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 DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
import WhatsappTemplates from 'dashboard/components/widgets/conversation/WhatsappTemplates/Modal.vue';
const props = defineProps({
show: {
@ -59,12 +60,21 @@ const isUpdating = computed(() => uiFlags.value.isUpdating);
const isSubmitting = computed(() => isCreating.value || isUpdating.value);
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 scheduledDateTime = ref(null);
const attachments = ref([]);
const existingAttachment = ref(null);
const templateParams = ref(null);
const showConfirmClose = ref(false);
const showWhatsAppTemplatesModal = ref(false);
const datePickerOpen = ref(false);
const contentError = ref(false);
const contentLengthError = ref(false);
@ -172,7 +182,18 @@ const hasTemplate = computed(
() => templateParams.value && Object.keys(templateParams.value).length
);
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 channelType = currentInbox.value?.channel_type;
@ -354,6 +375,21 @@ const closeModal = () => {
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 => {
if (!validatePayload(status)) return;
@ -458,14 +494,16 @@ watch(
<WootMessageEditor
v-model="messageContent"
class="message-editor min-h-[10rem] max-h-[20rem] !px-3 resize-y overflow-auto"
:class="
:class="[
contentError || contentLengthError
? 'border border-n-ruby-9 rounded-xl'
: ''
"
: '',
hasTemplate ? 'opacity-60 cursor-not-allowed' : '',
]"
:placeholder="t('SCHEDULED_MESSAGES.MODAL.MESSAGE_PLACEHOLDER')"
:channel-type="currentInbox?.channel_type"
:medium="currentInbox?.medium"
:disabled="hasTemplate"
override-line-breaks
@update:model-value="
() => {
@ -518,7 +556,7 @@ watch(
@change="onDateTimeChange"
/>
</div>
<div v-if="showAttachmentUpload" class="flex items-center h-10">
<div v-if="showAttachmentUpload" class="flex items-center gap-2 h-10">
<FileUpload
:accept="ALLOWED_FILE_TYPES"
:multiple="false"
@ -534,6 +572,33 @@ watch(
class="pointer-events-none"
/>
</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>
<span
v-if="existingAttachment && !attachments.length"
@ -632,5 +697,13 @@ watch(
</div>
</div>
</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>
</template>