mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Custom fields in pre-chat form (#4189)
This commit is contained in:
		| @@ -17,8 +17,12 @@ export default { | ||||
|     value: { type: Boolean, default: false }, | ||||
|   }, | ||||
|   methods: { | ||||
|     onClick() { | ||||
|     onClick(event) { | ||||
|       if (event.pointerId === -1) { | ||||
|         event.preventDefault(); | ||||
|       } else { | ||||
|         this.$emit('input', !this.value); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
							
								
								
									
										100
									
								
								app/javascript/dashboard/helper/preChat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								app/javascript/dashboard/helper/preChat.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| import i18n from 'widget/i18n/index'; | ||||
| const defaultTranslations = Object.fromEntries( | ||||
|   Object.entries(i18n).filter(([key]) => key.includes('en')) | ||||
| ).en; | ||||
|  | ||||
| export const standardFieldKeys = { | ||||
|   emailAddress: { | ||||
|     key: 'EMAIL_ADDRESS', | ||||
|     label: 'Email Id', | ||||
|     placeholder: 'Please enter your email address', | ||||
|   }, | ||||
|   fullName: { | ||||
|     key: 'FULL_NAME', | ||||
|     label: 'Full Name', | ||||
|     placeholder: 'Please enter your full name', | ||||
|   }, | ||||
|   phoneNumber: { | ||||
|     key: 'PHONE_NUMBER', | ||||
|     label: 'Phone Number', | ||||
|     placeholder: 'Please enter your phone number', | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const getLabel = ({ key, label }) => { | ||||
|   return defaultTranslations.PRE_CHAT_FORM.FIELDS[key] | ||||
|     ? defaultTranslations.PRE_CHAT_FORM.FIELDS[key].LABEL | ||||
|     : label; | ||||
| }; | ||||
| export const getPlaceHolder = ({ key, placeholder }) => { | ||||
|   return defaultTranslations.PRE_CHAT_FORM.FIELDS[key] | ||||
|     ? defaultTranslations.PRE_CHAT_FORM.FIELDS[key].PLACEHOLDER | ||||
|     : placeholder; | ||||
| }; | ||||
|  | ||||
| export const getCustomFields = ({ standardFields, customAttributes }) => { | ||||
|   let customFields = []; | ||||
|   const { pre_chat_fields: preChatFields } = standardFields; | ||||
|   customAttributes.forEach(attribute => { | ||||
|     const itemExist = preChatFields.find( | ||||
|       item => item.name === attribute.attribute_key | ||||
|     ); | ||||
|     if (!itemExist) { | ||||
|       customFields.push({ | ||||
|         label: attribute.attribute_display_name, | ||||
|         placeholder: attribute.attribute_display_name, | ||||
|         name: attribute.attribute_key, | ||||
|         type: attribute.attribute_display_type, | ||||
|         values: attribute.attribute_values, | ||||
|         field_type: attribute.attribute_model, | ||||
|         required: false, | ||||
|         enabled: false, | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|   return customFields; | ||||
| }; | ||||
|  | ||||
| export const getFormattedPreChatFields = ({ preChatFields }) => { | ||||
|   return preChatFields.map(item => { | ||||
|     return { | ||||
|       ...item, | ||||
|       label: getLabel({ | ||||
|         key: standardFieldKeys[item.name] | ||||
|           ? standardFieldKeys[item.name].key | ||||
|           : item.name, | ||||
|         label: item.label ? item.label : item.name, | ||||
|       }), | ||||
|       placeholder: getPlaceHolder({ | ||||
|         key: standardFieldKeys[item.name] | ||||
|           ? standardFieldKeys[item.name].key | ||||
|           : item.name, | ||||
|         placeholder: item.placeholder ? item.placeholder : item.name, | ||||
|       }), | ||||
|     }; | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| export const getPreChatFields = ({ | ||||
|   preChatFormOptions = {}, | ||||
|   customAttributes = [], | ||||
| }) => { | ||||
|   const { pre_chat_message, pre_chat_fields } = preChatFormOptions; | ||||
|   let customFields = {}; | ||||
|   let preChatFields = {}; | ||||
|  | ||||
|   const formattedPreChatFields = getFormattedPreChatFields({ | ||||
|     preChatFields: pre_chat_fields, | ||||
|   }); | ||||
|  | ||||
|   customFields = getCustomFields({ | ||||
|     standardFields: { pre_chat_fields: formattedPreChatFields }, | ||||
|     customAttributes, | ||||
|   }); | ||||
|   preChatFields = [...formattedPreChatFields, ...customFields]; | ||||
|  | ||||
|   return { | ||||
|     pre_chat_message, | ||||
|     pre_chat_fields: preChatFields, | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										47
									
								
								app/javascript/dashboard/helper/specs/inboxFixture.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/javascript/dashboard/helper/specs/inboxFixture.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| export default { | ||||
|   customFields: { | ||||
|     pre_chat_message: 'Share your queries or comments here.', | ||||
|     pre_chat_fields: [ | ||||
|       { | ||||
|         label: 'Email Address', | ||||
|         name: 'emailAddress', | ||||
|         type: 'email', | ||||
|         field_type: 'standard', | ||||
|         required: false, | ||||
|         enabled: false, | ||||
|  | ||||
|         placeholder: 'Please enter your email address', | ||||
|       }, | ||||
|       { | ||||
|         label: 'Full Name', | ||||
|         name: 'fullName', | ||||
|         type: 'text', | ||||
|         field_type: 'standard', | ||||
|         required: false, | ||||
|         enabled: false, | ||||
|         placeholder: 'Please enter your full name', | ||||
|       }, | ||||
|       { | ||||
|         label: 'Phone Number', | ||||
|         name: 'phoneNumber', | ||||
|         type: 'text', | ||||
|         field_type: 'standard', | ||||
|         required: false, | ||||
|         enabled: false, | ||||
|         placeholder: 'Please enter your phone number', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   customAttributes: [ | ||||
|     { | ||||
|       id: 101, | ||||
|       attribute_description: 'Order Identifier', | ||||
|       attribute_display_name: 'Order Id', | ||||
|       attribute_display_type: 'number', | ||||
|       attribute_key: 'order_id', | ||||
|       attribute_model: 'conversation_attribute', | ||||
|       attribute_values: Array(0), | ||||
|       created_at: '2021-11-29T10:20:04.563Z', | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
							
								
								
									
										76
									
								
								app/javascript/dashboard/helper/specs/preChat.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								app/javascript/dashboard/helper/specs/preChat.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import { | ||||
|   getPreChatFields, | ||||
|   getFormattedPreChatFields, | ||||
|   getCustomFields, | ||||
| } from '../preChat'; | ||||
| import inboxFixture from './inboxFixture'; | ||||
|  | ||||
| const { customFields, customAttributes } = inboxFixture; | ||||
| describe('#Pre chat Helpers', () => { | ||||
|   describe('getPreChatFields', () => { | ||||
|     it('should return correct pre-chat fields form options passed', () => { | ||||
|       expect(getPreChatFields({ preChatFormOptions: customFields })).toEqual( | ||||
|         customFields | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
|   describe('getFormattedPreChatFields', () => { | ||||
|     it('should return correct custom fields', () => { | ||||
|       expect( | ||||
|         getFormattedPreChatFields({ | ||||
|           preChatFields: customFields.pre_chat_fields, | ||||
|         }) | ||||
|       ).toEqual([ | ||||
|         { | ||||
|           label: 'Email Address', | ||||
|           name: 'emailAddress', | ||||
|           placeholder: 'Please enter your email address', | ||||
|           type: 'email', | ||||
|           field_type: 'standard', | ||||
|  | ||||
|           required: false, | ||||
|           enabled: false, | ||||
|         }, | ||||
|         { | ||||
|           label: 'Full Name', | ||||
|           name: 'fullName', | ||||
|           placeholder: 'Please enter your full name', | ||||
|           type: 'text', | ||||
|           field_type: 'standard', | ||||
|           required: false, | ||||
|           enabled: false, | ||||
|         }, | ||||
|         { | ||||
|           label: 'Phone Number', | ||||
|           name: 'phoneNumber', | ||||
|           placeholder: 'Please enter your phone number', | ||||
|           type: 'text', | ||||
|           field_type: 'standard', | ||||
|           required: false, | ||||
|           enabled: false, | ||||
|         }, | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
|   describe('getCustomFields', () => { | ||||
|     it('should return correct custom fields', () => { | ||||
|       expect( | ||||
|         getCustomFields({ | ||||
|           standardFields: { pre_chat_fields: customFields.pre_chat_fields }, | ||||
|           customAttributes, | ||||
|         }) | ||||
|       ).toEqual([ | ||||
|         { | ||||
|           enabled: false, | ||||
|           label: 'Order Id', | ||||
|           placeholder: 'Order Id', | ||||
|           name: 'order_id', | ||||
|           required: false, | ||||
|           field_type: 'conversation_attribute', | ||||
|           type: 'number', | ||||
|           values: [], | ||||
|         }, | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -211,7 +211,6 @@ | ||||
|           "PLACEHOLDER": "API key", | ||||
|           "APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here", | ||||
|           "ERROR": "Please enter a valid value." | ||||
|  | ||||
|         }, | ||||
|         "SUBMIT_BUTTON": "Create WhatsApp Channel", | ||||
|         "API": { | ||||
| @@ -433,6 +432,15 @@ | ||||
|     }, | ||||
|     "PRE_CHAT_FORM": { | ||||
|       "DESCRIPTION": "Pre chat forms enable you to capture user information before they start conversation with you.", | ||||
|       "SET_FIELDS": "Pre chat form fields", | ||||
|       "SET_FIELDS_HEADER": { | ||||
|         "FIELDS": "Fields", | ||||
|         "LABEL": "Label", | ||||
|         "PLACE_HOLDER":"Placeholder", | ||||
|         "KEY": "Key", | ||||
|         "TYPE": "Type", | ||||
|         "REQUIRED": "Required" | ||||
|       }, | ||||
|       "ENABLE": { | ||||
|         "LABEL": "Enable pre chat form", | ||||
|         "OPTIONS": { | ||||
| @@ -441,7 +449,7 @@ | ||||
|         } | ||||
|       }, | ||||
|       "PRE_CHAT_MESSAGE": { | ||||
|         "LABEL": "Pre Chat Message", | ||||
|         "LABEL": "Pre chat message", | ||||
|         "PLACEHOLDER": "This message would be visible to the users along with the form" | ||||
|       }, | ||||
|       "REQUIRE_EMAIL": { | ||||
| @@ -465,7 +473,7 @@ | ||||
|         "VALIDATION_ERROR": "Starting time should be before closing time.", | ||||
|         "CHOOSE": "Choose" | ||||
|       }, | ||||
|       "ALL_DAY":"All-Day" | ||||
|       "ALL_DAY": "All-Day" | ||||
|     }, | ||||
|     "IMAP": { | ||||
|       "TITLE": "IMAP", | ||||
|   | ||||
| @@ -0,0 +1,91 @@ | ||||
| <template> | ||||
|   <draggable v-model="preChatFields" tag="tbody"> | ||||
|     <tr v-for="(item, index) in preChatFields" :key="index"> | ||||
|       <td class="pre-chat-field"><fluent-icon icon="drag" /></td> | ||||
|       <td class="pre-chat-field"> | ||||
|         <woot-switch | ||||
|           :value="item['enabled']" | ||||
|           @input="handlePreChatFieldOptions($event, 'enabled', item)" | ||||
|         /> | ||||
|       </td> | ||||
|       <td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }"> | ||||
|         {{ item.name }} | ||||
|       </td> | ||||
|       <td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }"> | ||||
|         {{ item.type }} | ||||
|       </td> | ||||
|       <td class="pre-chat-field"> | ||||
|         <input | ||||
|           v-model="item['required']" | ||||
|           type="checkbox" | ||||
|           :value="`${item.name}-required`" | ||||
|           :disabled="!item['enabled']" | ||||
|           @click="handlePreChatFieldOptions($event, 'required', item)" | ||||
|         /> | ||||
|       </td> | ||||
|       <td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }"> | ||||
|         <input | ||||
|           v-model.trim="item.label" | ||||
|           type="text" | ||||
|           :disabled="isFieldEditable(item)" | ||||
|         /> | ||||
|       </td> | ||||
|       <td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }"> | ||||
|         <input | ||||
|           v-model.trim="item.placeholder" | ||||
|           type="text" | ||||
|           :disabled="isFieldEditable(item)" | ||||
|         /> | ||||
|       </td> | ||||
|     </tr> | ||||
|   </draggable> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import draggable from 'vuedraggable'; | ||||
| import { standardFieldKeys } from 'dashboard/helper/preChat'; | ||||
| export default { | ||||
|   components: { draggable }, | ||||
|   props: { | ||||
|     preChatFields: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     handlePreChatFieldOptions: { | ||||
|       type: Function, | ||||
|       default: () => {}, | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     isFieldEditable(item) { | ||||
|       return !!standardFieldKeys[item.name] || !item.enabled; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style scoped lang="scss"> | ||||
| .pre-chat-field { | ||||
|   padding: var(--space-normal) var(--space-small); | ||||
|  | ||||
|   svg { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|   } | ||||
| } | ||||
| .disabled-text { | ||||
|   color: var(--s-500); | ||||
| } | ||||
|  | ||||
| table { | ||||
|   thead th { | ||||
|     text-transform: none; | ||||
|   } | ||||
|   input { | ||||
|     font-size: var(--font-size-small); | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
| checkbox { | ||||
|   margin: 0; | ||||
| } | ||||
| </style> | ||||
| @@ -1,10 +1,10 @@ | ||||
| <template> | ||||
|   <div class="settings--content"> | ||||
|     <div class="prechat--title"> | ||||
|     <div class="pre-chat--title"> | ||||
|       {{ $t('INBOX_MGMT.PRE_CHAT_FORM.DESCRIPTION') }} | ||||
|     </div> | ||||
|     <form class="medium-6" @submit.prevent="updateInbox"> | ||||
|       <label class="medium-9 columns"> | ||||
|     <form @submit.prevent="updateInbox"> | ||||
|       <label class="medium-3 columns"> | ||||
|         {{ $t('INBOX_MGMT.PRE_CHAT_FORM.ENABLE.LABEL') }} | ||||
|         <select v-model="preChatFormEnabled"> | ||||
|           <option :value="true"> | ||||
| @@ -15,8 +15,8 @@ | ||||
|           </option> | ||||
|         </select> | ||||
|       </label> | ||||
|  | ||||
|       <label class="medium-9"> | ||||
|       <div v-if="preChatFormEnabled"> | ||||
|         <label class="medium-3 columns"> | ||||
|           {{ $t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.LABEL') }} | ||||
|           <textarea | ||||
|             v-model.trim="preChatMessage" | ||||
| @@ -26,17 +26,44 @@ | ||||
|             " | ||||
|           /> | ||||
|         </label> | ||||
|       <div> | ||||
|         <input | ||||
|           v-model="preChatFieldOptions" | ||||
|           type="checkbox" | ||||
|           value="requireEmail" | ||||
|           @input="handlePreChatFieldOptions" | ||||
|         <label class="medium-8 columns"> | ||||
|           {{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS') }} | ||||
|           <table class="table table-striped w-full"> | ||||
|             <thead class="thead-dark"> | ||||
|               <tr> | ||||
|                 <th scope="col"></th> | ||||
|                 <th scope="col"></th> | ||||
|                 <th scope="col"> | ||||
|                   {{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.KEY') }} | ||||
|                 </th> | ||||
|                 <th scope="col"> | ||||
|                   {{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.TYPE') }} | ||||
|                 </th> | ||||
|                 <th scope="col"> | ||||
|                   {{ | ||||
|                     $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.REQUIRED') | ||||
|                   }} | ||||
|                 </th> | ||||
|                 <th scope="col"> | ||||
|                   {{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.LABEL') }} | ||||
|                 </th> | ||||
|                 <th scope="col"> | ||||
|                   {{ | ||||
|                     $t( | ||||
|                       'INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.PLACE_HOLDER' | ||||
|                     ) | ||||
|                   }} | ||||
|                 </th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <pre-chat-fields | ||||
|               :pre-chat-fields="preChatFields" | ||||
|               :handle-pre-chat-field-options="handlePreChatFieldOptions" | ||||
|             /> | ||||
|         <label for="requireEmail"> | ||||
|           {{ $t('INBOX_MGMT.PRE_CHAT_FORM.REQUIRE_EMAIL.LABEL') }} | ||||
|           </table> | ||||
|         </label> | ||||
|       </div> | ||||
|  | ||||
|       <woot-submit-button | ||||
|         :button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')" | ||||
|         :loading="uiFlags.isUpdatingInbox" | ||||
| @@ -47,8 +74,13 @@ | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import alertMixin from 'shared/mixins/alertMixin'; | ||||
| import PreChatFields from './PreChatFields.vue'; | ||||
| import { getPreChatFields, standardFieldKeys } from 'dashboard/helper/preChat'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     PreChatFields, | ||||
|   }, | ||||
|   mixins: [alertMixin], | ||||
|   props: { | ||||
|     inbox: { | ||||
| @@ -60,11 +92,21 @@ export default { | ||||
|     return { | ||||
|       preChatFormEnabled: false, | ||||
|       preChatMessage: '', | ||||
|       preChatFieldOptions: [], | ||||
|       preChatFields: [], | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ uiFlags: 'inboxes/getUIFlags' }), | ||||
|     ...mapGetters({ | ||||
|       uiFlags: 'inboxes/getUIFlags', | ||||
|       customAttributes: 'attributes/getAttributes', | ||||
|     }), | ||||
|     preChatFieldOptions() { | ||||
|       const { pre_chat_form_options: preChatFormOptions } = this.inbox; | ||||
|       return getPreChatFields({ | ||||
|         preChatFormOptions, | ||||
|         customAttributes: this.customAttributes, | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     inbox() { | ||||
| @@ -76,25 +118,26 @@ export default { | ||||
|   }, | ||||
|   methods: { | ||||
|     setDefaults() { | ||||
|       const { | ||||
|         pre_chat_form_enabled: preChatFormEnabled, | ||||
|         pre_chat_form_options: preChatFormOptions, | ||||
|       } = this.inbox; | ||||
|       const { pre_chat_form_enabled: preChatFormEnabled } = this.inbox; | ||||
|       this.preChatFormEnabled = preChatFormEnabled; | ||||
|       const { pre_chat_message: preChatMessage, require_email: requireEmail } = | ||||
|         preChatFormOptions || {}; | ||||
|       const { | ||||
|         pre_chat_message: preChatMessage, | ||||
|         pre_chat_fields: preChatFields, | ||||
|       } = this.preChatFieldOptions || {}; | ||||
|       this.preChatMessage = preChatMessage; | ||||
|       if (requireEmail) { | ||||
|         this.preChatFieldOptions = ['requireEmail']; | ||||
|       } | ||||
|       this.preChatFields = preChatFields; | ||||
|     }, | ||||
|     handlePreChatFieldOptions(event) { | ||||
|       if (this.preChatFieldOptions.includes(event.target.value)) { | ||||
|         this.preChatFieldOptions = []; | ||||
|       } else { | ||||
|         this.preChatFieldOptions = [event.target.value]; | ||||
|       } | ||||
|     isFieldEditable(item) { | ||||
|       return !!standardFieldKeys[item.name] || !item.enabled; | ||||
|     }, | ||||
|     handlePreChatFieldOptions(event, type, item) { | ||||
|       this.preChatFields.forEach((field, index) => { | ||||
|         if (field.name === item.name) { | ||||
|           this.preChatFields[index][type] = !item[type]; | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     async updateInbox() { | ||||
|       try { | ||||
|         const payload = { | ||||
| @@ -104,7 +147,7 @@ export default { | ||||
|             pre_chat_form_enabled: this.preChatFormEnabled, | ||||
|             pre_chat_form_options: { | ||||
|               pre_chat_message: this.preChatMessage, | ||||
|               require_email: this.preChatFieldOptions.includes('requireEmail'), | ||||
|               pre_chat_fields: this.preChatFields, | ||||
|             }, | ||||
|           }, | ||||
|         }; | ||||
| @@ -117,12 +160,11 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style scoped> | ||||
| <style scoped lang="scss"> | ||||
| .settings--content { | ||||
|   font-size: var(--font-size-default); | ||||
| } | ||||
|  | ||||
| .prechat--title { | ||||
| .pre-chat--title { | ||||
|   margin: var(--space-medium) 0 var(--space-slab); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -16,6 +16,9 @@ export const getters = { | ||||
|   getUIFlags(_state) { | ||||
|     return _state.uiFlags; | ||||
|   }, | ||||
|   getAttributes: _state => { | ||||
|     return _state.records; | ||||
|   }, | ||||
|   getAttributesByModel: _state => attributeModel => { | ||||
|     return _state.records.filter( | ||||
|       record => record.attribute_model === attributeModel | ||||
|   | ||||
| @@ -2,6 +2,26 @@ import { getters } from '../../attributes'; | ||||
| import attributesList from './fixtures'; | ||||
|  | ||||
| describe('#getters', () => { | ||||
|   it('getAttributes', () => { | ||||
|     const state = { records: attributesList }; | ||||
|     expect(getters.getAttributes(state)).toEqual([ | ||||
|       { | ||||
|         attribute_display_name: 'Language', | ||||
|         attribute_display_type: 1, | ||||
|         attribute_description: 'The conversation language', | ||||
|         attribute_key: 'language', | ||||
|         attribute_model: 0, | ||||
|       }, | ||||
|       { | ||||
|         attribute_display_name: 'Language one', | ||||
|         attribute_display_type: 2, | ||||
|         attribute_description: 'The conversation language one', | ||||
|         attribute_key: 'language_one', | ||||
|         attribute_model: 1, | ||||
|       }, | ||||
|     ]); | ||||
|   }); | ||||
|  | ||||
|   it('getAttributesByModel', () => { | ||||
|     const state = { records: attributesList }; | ||||
|     expect(getters.getAttributesByModel(state)(1)).toEqual([ | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import Vue from 'vue'; | ||||
| import Vuelidate from 'vuelidate'; | ||||
| import VueI18n from 'vue-i18n'; | ||||
| import VueFormulate from '@braid/vue-formulate'; | ||||
| import store from '../widget/store'; | ||||
| import App from '../widget/App.vue'; | ||||
| import ActionCableConnector from '../widget/helpers/actionCable'; | ||||
| import i18n from '../widget/i18n'; | ||||
|  | ||||
| import { isPhoneE164OrEmpty } from 'shared/helpers/Validators'; | ||||
| import router from '../widget/router'; | ||||
| Vue.use(VueI18n); | ||||
| Vue.use(Vuelidate); | ||||
| @@ -14,7 +15,15 @@ const i18nConfig = new VueI18n({ | ||||
|   locale: 'en', | ||||
|   messages: i18n, | ||||
| }); | ||||
|  | ||||
| Vue.use(VueFormulate, { | ||||
|   rules: { | ||||
|     isPhoneE164OrEmpty: ({ value }) => isPhoneE164OrEmpty(value), | ||||
|   }, | ||||
|   classes: { | ||||
|     outer: 'mb-4 wrapper', | ||||
|     error: 'text-red-400 mt-2 text-xs font-medium', | ||||
|   }, | ||||
| }); | ||||
| // Event Bus | ||||
| window.bus = new Vue(); | ||||
|  | ||||
|   | ||||
| @@ -126,5 +126,6 @@ | ||||
|   "brand-whatsapp-outline": "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z", | ||||
|   "brand-github-outline": "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385c.6.105.825-.255.825-.57c0-.285-.015-1.23-.015-2.235c-3.015.555-3.795-.735-4.035-1.41c-.135-.345-.72-1.41-1.23-1.695c-.42-.225-1.02-.78-.015-.795c.945-.015 1.62.87 1.845 1.23c1.08 1.815 2.805 1.305 3.495.99c.105-.78.42-1.305.765-1.605c-2.67-.3-5.46-1.335-5.46-5.925c0-1.305.465-2.385 1.23-3.225c-.12-.3-.54-1.53.12-3.18c0 0 1.005-.315 3.3 1.23c.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23c.66 1.65.24 2.88.12 3.18c.765.84 1.23 1.905 1.23 3.225c0 4.605-2.805 5.625-5.475 5.925c.435.375.81 1.095.81 2.22c0 1.605-.015 2.895-.015 3.3c0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z", | ||||
|   "add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z", | ||||
|   "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z" | ||||
|   "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z", | ||||
|   "drag-outline": "M15 3.707V8.5a.5.5 0 0 0 1 0V3.707l1.146 1.147a.5.5 0 0 0 .708-.708l-2-2a.499.499 0 0 0-.708 0l-2 2a.5.5 0 0 0 .708.708L15 3.707ZM2 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm0 5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm.5 4.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM15 16.293V11.5a.5.5 0 0 1 1 0v4.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L15 16.293Z" | ||||
| } | ||||
|   | ||||
| @@ -151,10 +151,7 @@ export default { | ||||
|     }, | ||||
|     registerCampaignEvents() { | ||||
|       bus.$on(ON_CAMPAIGN_MESSAGE_CLICK, () => { | ||||
|         const showPreChatForm = | ||||
|           this.preChatFormEnabled && this.preChatFormOptions.requireEmail; | ||||
|         const isUserEmailAvailable = !!this.currentUser.email; | ||||
|         if (showPreChatForm && !isUserEmailAvailable) { | ||||
|         if (this.shouldShowPreChatForm) { | ||||
|           this.replaceRoute('prechat-form'); | ||||
|         } else { | ||||
|           this.replaceRoute('messages'); | ||||
|   | ||||
| @@ -1,47 +1,44 @@ | ||||
| <template> | ||||
|   <form | ||||
|   <FormulateForm | ||||
|     v-model="formValues" | ||||
|     class="flex flex-1 flex-col p-6 overflow-y-auto" | ||||
|     @submit.prevent="onSubmit" | ||||
|     @submit="onSubmit" | ||||
|   > | ||||
|     <div | ||||
|       v-if="shouldShowHeaderMessage" | ||||
|       class="text-sm leading-5" | ||||
|       class="mb-4 text-sm leading-5" | ||||
|       :class="$dm('text-black-800', 'dark:text-slate-50')" | ||||
|     > | ||||
|       {{ headerMessage }} | ||||
|     </div> | ||||
|     <form-input | ||||
|       v-if="areContactFieldsVisible" | ||||
|       v-model="fullName" | ||||
|       class="mt-5" | ||||
|       :label="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.LABEL')" | ||||
|       :placeholder="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.PLACEHOLDER')" | ||||
|       type="text" | ||||
|       :error=" | ||||
|         $v.fullName.$error ? $t('PRE_CHAT_FORM.FIELDS.FULL_NAME.ERROR') : '' | ||||
|       " | ||||
|     <FormulateInput | ||||
|       v-for="item in enabledPreChatFields" | ||||
|       :key="item.name" | ||||
|       :name="item.name" | ||||
|       :type="item.type" | ||||
|       :label="getLabel(item)" | ||||
|       :placeholder="getPlaceHolder(item)" | ||||
|       :validation="getValidation(item)" | ||||
|       :options="getOptions(item)" | ||||
|       :label-class="context => labelClass(context)" | ||||
|       :input-class="context => inputClass(context)" | ||||
|       :validation-messages="{ | ||||
|         isPhoneE164OrEmpty: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'), | ||||
|         email: $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'), | ||||
|         required: getRequiredErrorMessage(item), | ||||
|       }" | ||||
|     /> | ||||
|     <form-input | ||||
|       v-if="areContactFieldsVisible" | ||||
|       v-model="emailAddress" | ||||
|       class="mt-5" | ||||
|       :label="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.LABEL')" | ||||
|       :placeholder="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.PLACEHOLDER')" | ||||
|       type="email" | ||||
|       :error=" | ||||
|         $v.emailAddress.$error | ||||
|           ? $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.ERROR') | ||||
|           : '' | ||||
|       " | ||||
|     /> | ||||
|     <form-text-area | ||||
|     <FormulateInput | ||||
|       v-if="!hasActiveCampaign" | ||||
|       v-model="message" | ||||
|       class="my-5" | ||||
|       name="message" | ||||
|       type="textarea" | ||||
|       :label-class="context => labelClass(context)" | ||||
|       :input-class="context => inputClass(context)" | ||||
|       :label="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.LABEL')" | ||||
|       :placeholder="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.PLACEHOLDER')" | ||||
|       :error="$v.message.$error ? $t('PRE_CHAT_FORM.FIELDS.MESSAGE.ERROR') : ''" | ||||
|       validation="required" | ||||
|     /> | ||||
|  | ||||
|     <custom-button | ||||
|       class="font-medium my-5" | ||||
|       block | ||||
| @@ -52,24 +49,20 @@ | ||||
|       <spinner v-if="isCreating" class="p-0" /> | ||||
|       {{ $t('START_CONVERSATION') }} | ||||
|     </custom-button> | ||||
|   </form> | ||||
|   </FormulateForm> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import CustomButton from 'shared/components/Button'; | ||||
| import FormInput from '../Form/Input'; | ||||
| import FormTextArea from '../Form/TextArea'; | ||||
| import Spinner from 'shared/components/Spinner'; | ||||
| import { mapGetters } from 'vuex'; | ||||
| import { getContrastingTextColor } from '@chatwoot/utils'; | ||||
| import { required, minLength, email } from 'vuelidate/lib/validators'; | ||||
|  | ||||
| import { isEmptyObject } from 'widget/helpers/utils'; | ||||
| import routerMixin from 'widget/mixins/routerMixin'; | ||||
| import darkModeMixin from 'widget/mixins/darkModeMixin'; | ||||
| export default { | ||||
|   components: { | ||||
|     FormInput, | ||||
|     FormTextArea, | ||||
|     CustomButton, | ||||
|     Spinner, | ||||
|   }, | ||||
| @@ -77,47 +70,23 @@ export default { | ||||
|   props: { | ||||
|     options: { | ||||
|       type: Object, | ||||
|       default: () => ({}), | ||||
|       default: () => {}, | ||||
|     }, | ||||
|     disableContactFields: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   validations() { | ||||
|     const identityValidations = { | ||||
|       fullName: { | ||||
|         required, | ||||
|       }, | ||||
|       emailAddress: { | ||||
|         required, | ||||
|         email, | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
|     const messageValidation = { | ||||
|       message: { | ||||
|         required, | ||||
|         minLength: minLength(1), | ||||
|       }, | ||||
|     }; | ||||
|     // For campaign, message field is not required | ||||
|     if (this.hasActiveCampaign) { | ||||
|       return identityValidations; | ||||
|     } | ||||
|     if (this.areContactFieldsVisible) { | ||||
|       return { | ||||
|         ...identityValidations, | ||||
|         ...messageValidation, | ||||
|       }; | ||||
|     } | ||||
|     return messageValidation; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       fullName: '', | ||||
|       emailAddress: '', | ||||
|       locale: this.$root.$i18n.locale, | ||||
|       message: '', | ||||
|       formValues: {}, | ||||
|       labels: { | ||||
|         emailAddress: 'EMAIL_ADDRESS', | ||||
|         fullName: 'FULL_NAME', | ||||
|         phoneNumber: 'PHONE_NUMBER', | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
| @@ -125,6 +94,7 @@ export default { | ||||
|       widgetColor: 'appConfig/getWidgetColor', | ||||
|       isCreating: 'conversation/getIsCreating', | ||||
|       activeCampaign: 'campaign/getActiveCampaign', | ||||
|       currentUser: 'contacts/getCurrentUser', | ||||
|     }), | ||||
|     textColor() { | ||||
|       return getContrastingTextColor(this.widgetColor); | ||||
| @@ -141,23 +111,207 @@ export default { | ||||
|       } | ||||
|       return this.options.preChatMessage; | ||||
|     }, | ||||
|     areContactFieldsVisible() { | ||||
|       return this.options.requireEmail && !this.disableContactFields; | ||||
|     preChatFields() { | ||||
|       return this.disableContactFields ? [] : this.options.preChatFields; | ||||
|     }, | ||||
|     filteredPreChatFields() { | ||||
|       const isUserEmailAvailable = !!this.currentUser.email; | ||||
|       const isUserPhoneNumberAvailable = !!this.currentUser.phone_number; | ||||
|       return this.preChatFields.filter(field => { | ||||
|         if ( | ||||
|           (isUserEmailAvailable && field.name === 'emailAddress') || | ||||
|           (isUserPhoneNumberAvailable && field.name === 'phoneNumber') | ||||
|         ) { | ||||
|           return false; | ||||
|         } | ||||
|         return true; | ||||
|       }); | ||||
|     }, | ||||
|     enabledPreChatFields() { | ||||
|       return this.filteredPreChatFields | ||||
|         .filter(field => field.enabled) | ||||
|         .map(field => ({ | ||||
|           ...field, | ||||
|           type: this.findFieldType(field.type), | ||||
|         })); | ||||
|     }, | ||||
|     conversationCustomAttributes() { | ||||
|       let conversationAttributes = {}; | ||||
|       this.enabledPreChatFields.forEach(field => { | ||||
|         if (field.field_type === 'conversation_attribute') { | ||||
|           conversationAttributes = { | ||||
|             ...conversationAttributes, | ||||
|             [field.name]: this.getValue(field), | ||||
|           }; | ||||
|         } | ||||
|       }); | ||||
|       return conversationAttributes; | ||||
|     }, | ||||
|     contactCustomAttributes() { | ||||
|       let contactAttributes = {}; | ||||
|       this.enabledPreChatFields.forEach(field => { | ||||
|         if (field.field_type === 'contact_attribute') { | ||||
|           contactAttributes = { | ||||
|             ...contactAttributes, | ||||
|             [field.name]: this.getValue(field), | ||||
|           }; | ||||
|         } | ||||
|       }); | ||||
|       return contactAttributes; | ||||
|     }, | ||||
|     inputStyles() { | ||||
|       return `mt-2 border rounded w-full py-2 px-3 text-slate-700 outline-none`; | ||||
|     }, | ||||
|     isInputDarkOrLightMode() { | ||||
|       return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm( | ||||
|         'text-slate-700', | ||||
|         'dark:text-slate-50' | ||||
|       )}`; | ||||
|     }, | ||||
|     inputBorderColor() { | ||||
|       return `${this.$dm('border-black-200', 'dark:border-black-500')}`; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     onSubmit() { | ||||
|       this.$v.$touch(); | ||||
|       if (this.$v.$invalid) { | ||||
|         return; | ||||
|     labelClass(context) { | ||||
|       const { hasErrors } = context; | ||||
|       if (!hasErrors) { | ||||
|         return `text-xs font-medium ${this.$dm( | ||||
|           'text-black-800', | ||||
|           'dark:text-slate-50' | ||||
|         )}`; | ||||
|       } | ||||
|       return `text-xs font-medium ${this.$dm( | ||||
|         'text-red-400', | ||||
|         'dark:text-red-400' | ||||
|       )}`; | ||||
|     }, | ||||
|     inputClass(context) { | ||||
|       const { hasErrors, classification, type } = context; | ||||
|       if (classification === 'box' && type === 'checkbox') { | ||||
|         return ''; | ||||
|       } | ||||
|       if (!hasErrors) { | ||||
|         return `${this.inputStyles} hover:border-black-300 focus:border-black-300 ${this.isInputDarkOrLightMode} ${this.inputBorderColor}`; | ||||
|       } | ||||
|       return `${this.inputStyles} border-red-200 hover:border-red-300 focus:border-red-300 ${this.isInputDarkOrLightMode}`; | ||||
|     }, | ||||
|     isContactFieldRequired(field) { | ||||
|       return this.preChatFields.find(option => option.name === field).required; | ||||
|     }, | ||||
|     getLabel({ name, label }) { | ||||
|       if (this.labels[name]) | ||||
|         return this.$t(`PRE_CHAT_FORM.FIELDS.${this.labels[name]}.LABEL`); | ||||
|       return label; | ||||
|     }, | ||||
|     getPlaceHolder({ name, placeholder }) { | ||||
|       if (this.labels[name]) | ||||
|         return this.$t(`PRE_CHAT_FORM.FIELDS.${this.labels[name]}.PLACEHOLDER`); | ||||
|       return placeholder; | ||||
|     }, | ||||
|     getValue({ name, type }) { | ||||
|       if (type === 'select') { | ||||
|         return this.enabledPreChatFields.find(option => option.name === name) | ||||
|           .values[this.formValues[name]]; | ||||
|       } | ||||
|       return this.formValues[name] || null; | ||||
|     }, | ||||
|  | ||||
|     getRequiredErrorMessage({ name, label }) { | ||||
|       if (this.labels[name]) | ||||
|         return this.$t( | ||||
|           `PRE_CHAT_FORM.FIELDS.${this.labels[name]}.REQUIRED_ERROR` | ||||
|         ); | ||||
|       return `${label} ${this.$t('PRE_CHAT_FORM.IS_REQUIRED')}`; | ||||
|     }, | ||||
|     getValidation({ type, name }) { | ||||
|       if (!this.isContactFieldRequired(name)) { | ||||
|         return ''; | ||||
|       } | ||||
|       const validations = { | ||||
|         emailAddress: 'email', | ||||
|         phoneNumber: 'isPhoneE164OrEmpty', | ||||
|         url: 'url', | ||||
|         date: 'date', | ||||
|         text: null, | ||||
|         select: null, | ||||
|         number: null, | ||||
|       }; | ||||
|       const validationKeys = Object.keys(validations); | ||||
|       const validation = 'bail|required'; | ||||
|       if (validationKeys.includes(name) || validationKeys.includes(type)) { | ||||
|         const validationType = validations[type] || validations[name]; | ||||
|         return validationType ? `${validation}|${validationType}` : validation; | ||||
|       } | ||||
|       return ''; | ||||
|     }, | ||||
|     findFieldType(type) { | ||||
|       if (type === 'link') { | ||||
|         return 'url'; | ||||
|       } | ||||
|       if (type === 'list') { | ||||
|         return 'select'; | ||||
|       } | ||||
|  | ||||
|       return type; | ||||
|     }, | ||||
|     getOptions(item) { | ||||
|       if (item.type === 'select') { | ||||
|         let values = {}; | ||||
|         item.values.forEach((value, index) => { | ||||
|           values = { | ||||
|             ...values, | ||||
|             [index]: value, | ||||
|           }; | ||||
|         }); | ||||
|         return values; | ||||
|       } | ||||
|       return null; | ||||
|     }, | ||||
|     onSubmit() { | ||||
|       const { emailAddress, fullName, phoneNumber, message } = this.formValues; | ||||
|       const { email } = this.currentUser; | ||||
|       this.$emit('submit', { | ||||
|         fullName: this.fullName, | ||||
|         emailAddress: this.emailAddress, | ||||
|         message: this.message, | ||||
|         fullName, | ||||
|         phoneNumber, | ||||
|         emailAddress: emailAddress || email, | ||||
|         message, | ||||
|         activeCampaignId: this.activeCampaign.id, | ||||
|         conversationCustomAttributes: this.conversationCustomAttributes, | ||||
|         contactCustomAttributes: this.contactCustomAttributes, | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| ::v-deep { | ||||
|   .wrapper[data-type='checkbox'] { | ||||
|     .formulate-input-wrapper { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|  | ||||
|       label { | ||||
|         margin-left: 0.2rem; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   @media (prefers-color-scheme: dark) { | ||||
|     .wrapper { | ||||
|       .formulate-input-element--date, | ||||
|       .formulate-input-element--checkbox { | ||||
|         input { | ||||
|           color-scheme: dark; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   .wrapper[data-type='textarea'] { | ||||
|     .formulate-input-element--textarea { | ||||
|       textarea { | ||||
|         min-height: 8rem; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -44,12 +44,19 @@ | ||||
|       "FULL_NAME": { | ||||
|         "LABEL": "Full Name", | ||||
|         "PLACEHOLDER": "Please enter your full name", | ||||
|         "ERROR": "Full Name is required" | ||||
|         "REQUIRED_ERROR": "Full Name is required" | ||||
|       }, | ||||
|       "EMAIL_ADDRESS": { | ||||
|         "LABEL": "Email Address", | ||||
|         "PLACEHOLDER": "Please enter your email address", | ||||
|         "ERROR": "Invalid email address" | ||||
|         "REQUIRED_ERROR": "Email Address is required", | ||||
|         "VALID_ERROR": "Please enter a valid email address" | ||||
|       }, | ||||
|       "PHONE_NUMBER": { | ||||
|         "LABEL": "Phone Number", | ||||
|         "PLACEHOLDER": "Please enter your phone number", | ||||
|         "REQUIRED_ERROR": "Phone Number is required", | ||||
|         "VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555" | ||||
|       }, | ||||
|       "MESSAGE": { | ||||
|         "LABEL": "Message", | ||||
| @@ -57,7 +64,8 @@ | ||||
|         "ERROR": "Message too short" | ||||
|       } | ||||
|     }, | ||||
|     "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation" | ||||
|     "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation", | ||||
|     "IS_REQUIRED": "is required" | ||||
|   }, | ||||
|   "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", | ||||
|   "CHAT_FORM": { | ||||
|   | ||||
| @@ -25,15 +25,21 @@ export default { | ||||
|       return window.chatwootWebChannel.preChatFormEnabled; | ||||
|     }, | ||||
|     preChatFormOptions() { | ||||
|       let requireEmail = false; | ||||
|       let preChatMessage = ''; | ||||
|       const options = window.chatwootWebChannel.preChatFormOptions || {}; | ||||
|       requireEmail = options.require_email; | ||||
|       preChatMessage = options.pre_chat_message; | ||||
|       const { pre_chat_fields: preChatFields = [] } = options; | ||||
|       return { | ||||
|         requireEmail, | ||||
|         preChatMessage, | ||||
|         preChatFields, | ||||
|       }; | ||||
|     }, | ||||
|     shouldShowPreChatForm() { | ||||
|       const { preChatFields } = this.preChatFormOptions; | ||||
|       // Check if at least one enabled field in pre-chat fields | ||||
|       const hasEnabledFields = | ||||
|         preChatFields.filter(field => field.enabled).length > 0; | ||||
|       return this.preChatFormEnabled && hasEnabledFields; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,12 +1,30 @@ | ||||
| import { createWrapper } from '@vue/test-utils'; | ||||
| import configMixin from '../configMixin'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| const preChatFields = [ | ||||
|   { | ||||
|     label: 'Email Id', | ||||
|     name: 'emailAddress', | ||||
|     type: 'email', | ||||
|     field_type: 'standard', | ||||
|     required: false, | ||||
|     enabled: false, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Full name', | ||||
|     name: 'fullName', | ||||
|     type: 'text', | ||||
|     field_type: 'standard', | ||||
|     required: true, | ||||
|     enabled: true, | ||||
|   }, | ||||
| ]; | ||||
| global.chatwootWebChannel = { | ||||
|   avatarUrl: 'https://test.url', | ||||
|   hasAConnectedAgentBot: 'AgentBot', | ||||
|   enabledFeatures: ['emoji_picker', 'attachments', 'end_conversation'], | ||||
|   preChatFormOptions: { require_email: false, pre_chat_message: '' }, | ||||
|   preChatFormOptions: { pre_chat_fields: preChatFields, pre_chat_message: '' }, | ||||
|   preChatFormEnabled: true, | ||||
| }; | ||||
|  | ||||
| global.chatwootWidgetDefaults = { | ||||
| @@ -29,18 +47,22 @@ describe('configMixin', () => { | ||||
|     expect(wrapper.vm.hasAConnectedAgentBot).toBe(true); | ||||
|     expect(wrapper.vm.useInboxAvatarForBot).toBe(true); | ||||
|     expect(wrapper.vm.inboxAvatarUrl).toBe('https://test.url'); | ||||
|  | ||||
|     expect(wrapper.vm.channelConfig).toEqual({ | ||||
|       avatarUrl: 'https://test.url', | ||||
|       hasAConnectedAgentBot: 'AgentBot', | ||||
|       enabledFeatures: ['emoji_picker', 'attachments', 'end_conversation'], | ||||
|       preChatFormOptions: { | ||||
|         pre_chat_message: '', | ||||
|         require_email: false, | ||||
|         pre_chat_fields: preChatFields, | ||||
|       }, | ||||
|       preChatFormEnabled: true, | ||||
|     }); | ||||
|     expect(wrapper.vm.preChatFormOptions).toEqual({ | ||||
|       requireEmail: false, | ||||
|       preChatMessage: '', | ||||
|       preChatFields: preChatFields, | ||||
|     }); | ||||
|     expect(wrapper.vm.preChatFormEnabled).toEqual(true); | ||||
|     expect(wrapper.vm.shouldShowPreChatForm).toEqual(true); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <div class="flex flex-1 flex-col justify-end"> | ||||
|     <div class="flex flex-1 overflow-auto"> | ||||
|       <!-- Load Converstion List Components Here --> | ||||
|       <!-- Load Conversation List Components Here --> | ||||
|     </div> | ||||
|     <team-availability | ||||
|       :available-agents="availableAgents" | ||||
| @@ -40,16 +40,12 @@ export default { | ||||
|       availableAgents: 'agent/availableAgents', | ||||
|       activeCampaign: 'campaign/getActiveCampaign', | ||||
|       conversationSize: 'conversation/getConversationSize', | ||||
|       currentUser: 'contacts/getCurrentUser', | ||||
|     }), | ||||
|   }, | ||||
|   methods: { | ||||
|     startConversation() { | ||||
|       const isUserEmailAvailable = !!this.currentUser.email; | ||||
|       if (this.preChatFormEnabled && !this.conversationSize) { | ||||
|         return this.replaceRoute('prechat-form', { | ||||
|           disableContactFields: isUserEmailAvailable, | ||||
|         }); | ||||
|         return this.replaceRoute('prechat-form'); | ||||
|       } | ||||
|       return this.replaceRoute('messages'); | ||||
|     }, | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { mapGetters } from 'vuex'; | ||||
| import PreChatForm from '../components/PreChat/Form'; | ||||
| import configMixin from '../mixins/configMixin'; | ||||
| import routerMixin from '../mixins/routerMixin'; | ||||
| import { isEmptyObject } from 'widget/helpers/utils'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
| @@ -35,13 +36,22 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     onSubmit({ fullName, emailAddress, message, activeCampaignId }) { | ||||
|     onSubmit({ | ||||
|       fullName, | ||||
|       emailAddress, | ||||
|       message, | ||||
|       activeCampaignId, | ||||
|       phoneNumber, | ||||
|       contactCustomAttributes, | ||||
|       conversationCustomAttributes, | ||||
|     }) { | ||||
|       if (activeCampaignId) { | ||||
|         bus.$emit('execute-campaign', activeCampaignId); | ||||
|         this.$store.dispatch('contacts/update', { | ||||
|           user: { | ||||
|             email: emailAddress, | ||||
|             name: fullName, | ||||
|             phone_number: phoneNumber, | ||||
|           }, | ||||
|         }); | ||||
|       } else { | ||||
| @@ -49,8 +59,16 @@ export default { | ||||
|           fullName: fullName, | ||||
|           emailAddress: emailAddress, | ||||
|           message: message, | ||||
|           phoneNumber: phoneNumber, | ||||
|           customAttributes: conversationCustomAttributes, | ||||
|         }); | ||||
|       } | ||||
|       if (!isEmptyObject(contactCustomAttributes)) { | ||||
|         this.$store.dispatch( | ||||
|           'contacts/setCustomAttributes', | ||||
|           contactCustomAttributes | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -32,9 +32,13 @@ class Channel::WebWidget < ApplicationRecord | ||||
|   self.table_name = 'channel_web_widgets' | ||||
|   EDITABLE_ATTRS = [:website_url, :widget_color, :welcome_title, :welcome_tagline, :reply_time, :pre_chat_form_enabled, | ||||
|                     :continuity_via_email, :hmac_mandatory, | ||||
|                     { pre_chat_form_options: [:pre_chat_message, :require_email] }, | ||||
|                     { pre_chat_form_options: [:pre_chat_message, :require_email, | ||||
|                                               { pre_chat_fields: | ||||
|                                                 [:field_type, :label, :placeholder, :name, :enabled, :type, :enabled, :required, | ||||
|                                                  :locale, { values: [] }] }] }, | ||||
|                     { selected_feature_flags: [] }].freeze | ||||
|  | ||||
|   before_validation :validate_pre_chat_options | ||||
|   validates :website_url, presence: true | ||||
|   validates :widget_color, presence: true | ||||
|  | ||||
| @@ -74,6 +78,25 @@ class Channel::WebWidget < ApplicationRecord | ||||
|     " | ||||
|   end | ||||
|  | ||||
|   def validate_pre_chat_options | ||||
|     return if pre_chat_form_options.with_indifferent_access['pre_chat_fields'].present? | ||||
|  | ||||
|     self.pre_chat_form_options = { | ||||
|       pre_chat_message: 'Share your queries or comments here.', | ||||
|       pre_chat_fields: [ | ||||
|         { | ||||
|           'field_type': 'standard', 'label': 'Email Id', 'name': 'emailAddress', 'type': 'email', 'required': true, 'enabled': false | ||||
|         }, | ||||
|         { | ||||
|           'field_type': 'standard', 'label': 'Full name', 'name': 'fullName', 'type': 'text', 'required': false, 'enabled': false | ||||
|         }, | ||||
|         { | ||||
|           'field_type': 'standard', 'label': 'Phone number', 'name': 'phoneNumber', 'type': 'text', 'required': false, 'enabled': false | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def create_contact_inbox(additional_attributes = {}) | ||||
|     ActiveRecord::Base.transaction do | ||||
|       contact = inbox.account.contacts.create!( | ||||
|   | ||||
| @@ -39,6 +39,6 @@ class CustomAttributeDefinition < ApplicationRecord | ||||
|   private | ||||
|  | ||||
|   def sync_widget_pre_chat_custom_fields | ||||
|     ::Inboxes::SyncWidgetPreChatCustomFieldsJob.perform_later(attribute_key) | ||||
|     ::Inboxes::SyncWidgetPreChatCustomFieldsJob.perform_now(account, attribute_key) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| json.id @contact.id | ||||
| json.name @contact.name | ||||
| json.email @contact.email | ||||
| json.phone_number @contact.phone_number | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| json.id @contact.id | ||||
| json.name @contact.name | ||||
| json.email @contact.email | ||||
| json.phone_number @contact.phone_number | ||||
|   | ||||
| @@ -0,0 +1,29 @@ | ||||
| class AddCustomFieldsToPreChatForm < ActiveRecord::Migration[6.1] | ||||
|   def change | ||||
|     Channel::WebWidget.find_in_batches do |channels_batch| | ||||
|       channels_batch.each do |channel| | ||||
|         pre_chat_message = channel[:pre_chat_form_options]['pre_chat_message'] || 'Share your queries or comments here.' | ||||
|         pre_chat_fields = pre_chat_fields?(channel) | ||||
|         channel[:pre_chat_form_options] = { | ||||
|           'pre_chat_message': pre_chat_message, | ||||
|           'pre_chat_fields': pre_chat_fields | ||||
|         } | ||||
|         channel.save! | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def pre_chat_fields?(channel) | ||||
|     email_enabled = channel[:pre_chat_form_options]['require_email'] || false | ||||
|     [ | ||||
|       { | ||||
|         'field_type': 'standard', 'label': 'Email Id', 'name': 'emailAddress', 'type': 'email', 'required': true, 'enabled': email_enabled | ||||
|       }, | ||||
|       { | ||||
|         'field_type': 'standard', 'label': 'Full name', 'name': 'fullName', 'type': 'text', 'required': false, 'enabled': false | ||||
|       }, { | ||||
|         'field_type': 'standard', 'label': 'Phone number', 'name': 'phoneNumber', 'type': 'text', 'required': false, 'enabled': false | ||||
|       } | ||||
|     ] | ||||
|   end | ||||
| end | ||||
							
								
								
									
										15
									
								
								spec/models/channel/web_widget_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								spec/models/channel/web_widget_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe Channel::WebWidget do | ||||
|   context 'when | ||||
|   web widget channel' do | ||||
|     let!(:channel_widget) { create(:channel_widget) } | ||||
|  | ||||
|     it 'pre chat options' do | ||||
|       expect(channel_widget.pre_chat_form_options['pre_chat_message']).to eq 'Share your queries or comments here.' | ||||
|       expect(channel_widget.pre_chat_form_options['pre_chat_fields'].length).to eq 3 | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -1,7 +0,0 @@ | ||||
| require 'test_helper' | ||||
|  | ||||
| class Channel::WidgetTest < ActiveSupport::TestCase | ||||
|   # test "the truth" do | ||||
|   #   assert true | ||||
|   # end | ||||
| end | ||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth