mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	 99997a701a
			
		
	
	99997a701a
	
	
	
		
			
			Implements comprehensive Twilio WhatsApp content template support (Phase
1) enabling text, media, and quick reply templates with proper parameter
conversion, sync capabilities, and feature flag protection.
###  Features Implemented
  **Template Types Supported**
  - Basic Text Templates: Simple text with variables ({{1}}, {{2}})
  - Media Templates: Image/Video/Document templates with text variables
  - Quick Reply Templates: Interactive button templates
- Phase 2 (Future): List Picker, Call-to-Action, Catalog, Carousel,
Authentication templates
  **Template Synchronization**
- API Endpoint: POST
/api/v1/accounts/{account_id}/inboxes/{inbox_id}/sync_templates
  - Background Job: Channels::Twilio::TemplatesSyncJob
  - Storage: JSONB format in channel_twilio_sms.content_templates
  - Auto-categorization: UTILITY, MARKETING, AUTHENTICATION categories
 ###  Template Examples Tested
  #### Text template
```
  { "name": "greet", "language": "en" }
```
  #### Template with variables
```
  { "name": "order_status", "parameters": [{"type": "body", "parameters": [{"text": "John"}]}] }
```
  #### Media template with image
```
  { "name": "product_showcase", "parameters": [
    {"type": "header", "parameters": [{"image": {"link": "image.jpg"}}]},
    {"type": "body", "parameters": [{"text": "iPhone"}, {"text": "$999"}]}
  ]}
```
#### Preview
<img width="1362" height="1058" alt="CleanShot 2025-08-26 at 10 01
51@2x"
src="https://github.com/user-attachments/assets/cb280cea-08c3-44ca-8025-58a96cb3a451"
/>
<img width="1308" height="1246" alt="CleanShot 2025-08-26 at 10 02
02@2x"
src="https://github.com/user-attachments/assets/9ea8537a-61e9-40f5-844f-eaad337e1ddd"
/>
#### User guide
https://www.chatwoot.com/hc/user-guide/articles/1756195741-twilio-content-templates
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
		
	
		
			
				
	
	
		
			125 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			125 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <script setup>
 | |
| import { computed, ref } from 'vue';
 | |
| import { useI18n } from 'vue-i18n';
 | |
| import { useMapGetter } from 'dashboard/composables/store';
 | |
| 
 | |
| import Icon from 'dashboard/components-next/icon/Icon.vue';
 | |
| import Button from 'dashboard/components-next/button/Button.vue';
 | |
| import Input from 'dashboard/components-next/input/Input.vue';
 | |
| import ContentTemplateForm from './ContentTemplateForm.vue';
 | |
| 
 | |
| const props = defineProps({
 | |
|   inboxId: {
 | |
|     type: Number,
 | |
|     required: true,
 | |
|   },
 | |
| });
 | |
| 
 | |
| const emit = defineEmits(['sendMessage']);
 | |
| 
 | |
| const { t } = useI18n();
 | |
| const inbox = useMapGetter('inboxes/getInbox');
 | |
| 
 | |
| const searchQuery = ref('');
 | |
| const selectedTemplate = ref(null);
 | |
| const showTemplatesMenu = ref(false);
 | |
| 
 | |
| const contentTemplates = computed(() => {
 | |
|   const inboxData = inbox.value(props.inboxId);
 | |
|   return inboxData?.content_templates?.templates || [];
 | |
| });
 | |
| 
 | |
| const filteredTemplates = computed(() => {
 | |
|   return contentTemplates.value.filter(
 | |
|     template =>
 | |
|       template.friendly_name
 | |
|         .toLowerCase()
 | |
|         .includes(searchQuery.value.toLowerCase()) &&
 | |
|       template.status === 'approved'
 | |
|   );
 | |
| });
 | |
| 
 | |
| const handleTriggerClick = () => {
 | |
|   searchQuery.value = '';
 | |
|   showTemplatesMenu.value = !showTemplatesMenu.value;
 | |
| };
 | |
| 
 | |
| const handleTemplateClick = template => {
 | |
|   selectedTemplate.value = template;
 | |
|   showTemplatesMenu.value = false;
 | |
| };
 | |
| 
 | |
| const handleBack = () => {
 | |
|   selectedTemplate.value = null;
 | |
|   showTemplatesMenu.value = true;
 | |
| };
 | |
| 
 | |
| const handleSendMessage = template => {
 | |
|   emit('sendMessage', template);
 | |
|   selectedTemplate.value = null;
 | |
| };
 | |
| </script>
 | |
| 
 | |
| <template>
 | |
|   <div class="relative">
 | |
|     <Button
 | |
|       icon="i-ph-whatsapp-logo"
 | |
|       :label="t('COMPOSE_NEW_CONVERSATION.FORM.TWILIO_OPTIONS.LABEL')"
 | |
|       color="slate"
 | |
|       size="sm"
 | |
|       :disabled="selectedTemplate"
 | |
|       class="!text-xs font-medium"
 | |
|       @click="handleTriggerClick"
 | |
|     />
 | |
|     <div
 | |
|       v-if="showTemplatesMenu"
 | |
|       class="absolute top-full mt-1.5 max-h-96 overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
 | |
|     >
 | |
|       <div class="w-full">
 | |
|         <Input
 | |
|           v-model="searchQuery"
 | |
|           type="search"
 | |
|           :placeholder="
 | |
|             t('COMPOSE_NEW_CONVERSATION.FORM.TWILIO_OPTIONS.SEARCH_PLACEHOLDER')
 | |
|           "
 | |
|           custom-input-class="ltr:pl-10 rtl:pr-10"
 | |
|         >
 | |
|           <template #prefix>
 | |
|             <Icon
 | |
|               icon="i-lucide-search"
 | |
|               class="absolute top-2 size-3.5 ltr:left-3 rtl:right-3"
 | |
|             />
 | |
|           </template>
 | |
|         </Input>
 | |
|       </div>
 | |
|       <div
 | |
|         v-for="template in filteredTemplates"
 | |
|         :key="template.content_sid"
 | |
|         tabindex="0"
 | |
|         class="flex flex-col gap-2 p-2 w-full rounded-lg cursor-pointer dark:hover:bg-n-alpha-3 hover:bg-n-alpha-1"
 | |
|         @click="handleTemplateClick(template)"
 | |
|       >
 | |
|         <div class="flex justify-between items-center">
 | |
|           <span class="text-sm text-n-slate-12">{{
 | |
|             template.friendly_name
 | |
|           }}</span>
 | |
|         </div>
 | |
|         <p class="mb-0 text-xs leading-5 text-n-slate-11 line-clamp-2">
 | |
|           {{ template.body || t('CONTENT_TEMPLATES.PICKER.NO_CONTENT') }}
 | |
|         </p>
 | |
|       </div>
 | |
|       <template v-if="filteredTemplates.length === 0">
 | |
|         <p class="pt-2 w-full text-sm text-n-slate-11">
 | |
|           {{ t('COMPOSE_NEW_CONVERSATION.FORM.TWILIO_OPTIONS.EMPTY_STATE') }}
 | |
|         </p>
 | |
|       </template>
 | |
|     </div>
 | |
|     <ContentTemplateForm
 | |
|       v-if="selectedTemplate"
 | |
|       :template="selectedTemplate"
 | |
|       @send-message="handleSendMessage"
 | |
|       @back="handleBack"
 | |
|     />
 | |
|   </div>
 | |
| </template>
 |