mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: Support variables in canned response (#6077)
- Added the option to insert variables in canned responses. - Populate variables on selecting a canned response. - Show a warning if there are any undefined variables in the message before sending a message.
This commit is contained in:
		| @@ -6,10 +6,15 @@ | ||||
|       @click="insertMentionNode" | ||||
|     /> | ||||
|     <canned-response | ||||
|       v-if="showCannedMenu && !isPrivate" | ||||
|       v-if="shouldShowCannedResponses" | ||||
|       :search-key="cannedSearchTerm" | ||||
|       @click="insertCannedResponse" | ||||
|     /> | ||||
|     <variable-list | ||||
|       v-if="shouldShowVariables" | ||||
|       :search-key="variableSearchTerm" | ||||
|       @click="insertVariable" | ||||
|     /> | ||||
|     <div ref="editor" /> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -31,6 +36,7 @@ import { | ||||
|  | ||||
| import TagAgents from '../conversation/TagAgents'; | ||||
| import CannedResponse from '../conversation/CannedResponse'; | ||||
| import VariableList from '../conversation/VariableList'; | ||||
|  | ||||
| const TYPING_INDICATOR_IDLE_TIME = 4000; | ||||
|  | ||||
| @@ -43,6 +49,7 @@ import { | ||||
| import eventListenerMixins from 'shared/mixins/eventListenerMixins'; | ||||
| import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | ||||
| import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; | ||||
| import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper'; | ||||
| import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; | ||||
|  | ||||
| const createState = (content, placeholder, plugins = []) => { | ||||
| @@ -58,7 +65,7 @@ const createState = (content, placeholder, plugins = []) => { | ||||
|  | ||||
| export default { | ||||
|   name: 'WootMessageEditor', | ||||
|   components: { TagAgents, CannedResponse }, | ||||
|   components: { TagAgents, CannedResponse, VariableList }, | ||||
|   mixins: [eventListenerMixins, uiSettingsMixin], | ||||
|   props: { | ||||
|     value: { type: String, default: '' }, | ||||
| @@ -68,13 +75,18 @@ export default { | ||||
|     enableSuggestions: { type: Boolean, default: true }, | ||||
|     overrideLineBreaks: { type: Boolean, default: false }, | ||||
|     updateSelectionWith: { type: String, default: '' }, | ||||
|     enableVariables: { type: Boolean, default: false }, | ||||
|     enableCannedResponses: { type: Boolean, default: true }, | ||||
|     variables: { type: Object, default: () => ({}) }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       showUserMentions: false, | ||||
|       showCannedMenu: false, | ||||
|       showVariables: false, | ||||
|       mentionSearchKey: '', | ||||
|       cannedSearchTerm: '', | ||||
|       variableSearchTerm: '', | ||||
|       editorView: null, | ||||
|       range: null, | ||||
|       state: undefined, | ||||
| @@ -84,6 +96,14 @@ export default { | ||||
|     contentFromEditor() { | ||||
|       return MessageMarkdownSerializer.serialize(this.editorView.state.doc); | ||||
|     }, | ||||
|     shouldShowVariables() { | ||||
|       return this.enableVariables && this.showVariables && !this.isPrivate; | ||||
|     }, | ||||
|     shouldShowCannedResponses() { | ||||
|       return ( | ||||
|         this.enableCannedResponses && this.showCannedMenu && !this.isPrivate | ||||
|       ); | ||||
|     }, | ||||
|     plugins() { | ||||
|       if (!this.enableSuggestions) { | ||||
|         return []; | ||||
| @@ -103,6 +123,7 @@ export default { | ||||
|             this.range = args.range; | ||||
|  | ||||
|             this.mentionSearchKey = args.text.replace('@', ''); | ||||
|  | ||||
|             return false; | ||||
|           }, | ||||
|           onExit: () => { | ||||
| @@ -142,6 +163,34 @@ export default { | ||||
|             return event.keyCode === 13 && this.showCannedMenu; | ||||
|           }, | ||||
|         }), | ||||
|         suggestionsPlugin({ | ||||
|           matcher: triggerCharacters('{{'), | ||||
|           suggestionClass: '', | ||||
|           onEnter: args => { | ||||
|             if (this.isPrivate) { | ||||
|               return false; | ||||
|             } | ||||
|             this.showVariables = true; | ||||
|             this.range = args.range; | ||||
|             this.editorView = args.view; | ||||
|             return false; | ||||
|           }, | ||||
|           onChange: args => { | ||||
|             this.editorView = args.view; | ||||
|             this.range = args.range; | ||||
|  | ||||
|             this.variableSearchTerm = args.text.replace('{{', ''); | ||||
|             return false; | ||||
|           }, | ||||
|           onExit: () => { | ||||
|             this.variableSearchTerm = ''; | ||||
|             this.showVariables = false; | ||||
|             return false; | ||||
|           }, | ||||
|           onKeyDown: ({ event }) => { | ||||
|             return event.keyCode === 13 && this.showVariables; | ||||
|           }, | ||||
|         }), | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| @@ -152,6 +201,9 @@ export default { | ||||
|     showCannedMenu(updatedValue) { | ||||
|       this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue); | ||||
|     }, | ||||
|     showVariables(updatedValue) { | ||||
|       this.$emit('toggle-variables-menu', !this.isPrivate && updatedValue); | ||||
|     }, | ||||
|     value(newValue = '') { | ||||
|       if (newValue !== this.contentFromEditor) { | ||||
|         this.reloadState(); | ||||
| @@ -267,19 +319,22 @@ export default { | ||||
|  | ||||
|       return false; | ||||
|     }, | ||||
|  | ||||
|     insertCannedResponse(cannedItem) { | ||||
|       const updatedMessage = replaceVariablesInMessage({ | ||||
|         message: cannedItem, | ||||
|         variables: this.variables, | ||||
|       }); | ||||
|       if (!this.editorView) { | ||||
|         return null; | ||||
|       } | ||||
|  | ||||
|       let from = this.range.from - 1; | ||||
|       let node = new MessageMarkdownTransformer(messageSchema).parse( | ||||
|         cannedItem | ||||
|         updatedMessage | ||||
|       ); | ||||
|  | ||||
|       if (node.textContent === cannedItem) { | ||||
|         node = this.editorView.state.schema.text(cannedItem); | ||||
|       if (node.textContent === updatedMessage) { | ||||
|         node = this.editorView.state.schema.text(updatedMessage); | ||||
|         from = this.range.from; | ||||
|       } | ||||
|  | ||||
| @@ -296,6 +351,29 @@ export default { | ||||
|       this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); | ||||
|       return false; | ||||
|     }, | ||||
|     insertVariable(variable) { | ||||
|       if (!this.editorView) { | ||||
|         return null; | ||||
|       } | ||||
|       let node = this.editorView.state.schema.text(`{{${variable}}}`); | ||||
|       const from = this.range.from; | ||||
|  | ||||
|       const tr = this.editorView.state.tr.replaceWith( | ||||
|         from, | ||||
|         this.range.to, | ||||
|         node | ||||
|       ); | ||||
|  | ||||
|       this.state = this.editorView.state.apply(tr); | ||||
|       this.emitOnChange(); | ||||
|  | ||||
|       // The `{{ }}` are added to the message, but the cursor is placed | ||||
|       // and onExit of suggestionsPlugin is not called. So we need to manually hide | ||||
|       this.showVariables = false; | ||||
|       this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE); | ||||
|       tr.scrollIntoView(); | ||||
|       return false; | ||||
|     }, | ||||
|  | ||||
|     emitOnChange() { | ||||
|       this.editorView.updateState(this.state); | ||||
|   | ||||
| @@ -63,12 +63,15 @@ | ||||
|         :placeholder="messagePlaceHolder" | ||||
|         :update-selection-with="updateEditorSelectionWith" | ||||
|         :min-height="4" | ||||
|         :enable-variables="true" | ||||
|         :variables="messageVariables" | ||||
|         @typing-off="onTypingOff" | ||||
|         @typing-on="onTypingOn" | ||||
|         @focus="onFocus" | ||||
|         @blur="onBlur" | ||||
|         @toggle-user-mention="toggleUserMention" | ||||
|         @toggle-canned-menu="toggleCannedMenu" | ||||
|         @toggle-variables-menu="toggleVariablesMenu" | ||||
|         @clear-selection="clearEditorSelection" | ||||
|       /> | ||||
|     </div> | ||||
| @@ -126,6 +129,12 @@ | ||||
|       @on-send="onSendWhatsAppReply" | ||||
|       @cancel="hideWhatsappTemplatesModal" | ||||
|     /> | ||||
|  | ||||
|     <woot-confirm-modal | ||||
|       ref="confirmDialog" | ||||
|       :title="$t('CONVERSATION.REPLYBOX.UNDEFINED_VARIABLES.TITLE')" | ||||
|       :description="undefinedVariableMessage" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @@ -152,7 +161,11 @@ import { | ||||
|   AUDIO_FORMATS, | ||||
| } from 'shared/constants/messages'; | ||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
|  | ||||
| import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper'; | ||||
| import { | ||||
|   getMessageVariables, | ||||
|   getUndefinedVariablesInMessage, | ||||
| } from 'dashboard/helper/messageHelper'; | ||||
| import WhatsappTemplates from './WhatsappTemplates/Modal.vue'; | ||||
| import { buildHotKeys } from 'shared/helpers/KeyboardHelpers'; | ||||
| import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper'; | ||||
| @@ -208,7 +221,6 @@ export default { | ||||
|       message: '', | ||||
|       isFocused: false, | ||||
|       showEmojiPicker: false, | ||||
|       showMentions: false, | ||||
|       attachedFiles: [], | ||||
|       isRecordingAudio: false, | ||||
|       recordingAudioState: '', | ||||
| @@ -216,13 +228,16 @@ export default { | ||||
|       isUploading: false, | ||||
|       replyType: REPLY_EDITOR_MODES.REPLY, | ||||
|       mentionSearchKey: '', | ||||
|       hasUserMention: false, | ||||
|       hasSlashCommand: false, | ||||
|       bccEmails: '', | ||||
|       ccEmails: '', | ||||
|       doAutoSaveDraft: () => {}, | ||||
|       showWhatsAppTemplatesModal: false, | ||||
|       updateEditorSelectionWith: '', | ||||
|       undefinedVariableMessage: '', | ||||
|       showMentions: false, | ||||
|       showCannedMenu: false, | ||||
|       showVariablesMenu: false, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
| @@ -469,6 +484,12 @@ export default { | ||||
|       } | ||||
|       return AUDIO_FORMATS.OGG; | ||||
|     }, | ||||
|     messageVariables() { | ||||
|       const variables = getMessageVariables({ | ||||
|         conversation: this.currentChat, | ||||
|       }); | ||||
|       return variables; | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     currentChat(conversation) { | ||||
| @@ -610,8 +631,9 @@ export default { | ||||
|     }, | ||||
|     isAValidEvent(selectedKey) { | ||||
|       return ( | ||||
|         !this.hasUserMention && | ||||
|         !this.showMentions && | ||||
|         !this.showCannedMenu && | ||||
|         !this.showVariablesMenu && | ||||
|         this.isFocused && | ||||
|         isEditorHotKeyEnabled(this.uiSettings, selectedKey) | ||||
|       ); | ||||
| @@ -630,11 +652,14 @@ export default { | ||||
|       }); | ||||
|     }, | ||||
|     toggleUserMention(currentMentionState) { | ||||
|       this.hasUserMention = currentMentionState; | ||||
|       this.showMentions = currentMentionState; | ||||
|     }, | ||||
|     toggleCannedMenu(value) { | ||||
|       this.showCannedMenu = value; | ||||
|     }, | ||||
|     toggleVariablesMenu(value) { | ||||
|       this.showVariablesMenu = value; | ||||
|     }, | ||||
|     openWhatsappTemplateModal() { | ||||
|       this.showWhatsAppTemplatesModal = true; | ||||
|     }, | ||||
| @@ -664,7 +689,7 @@ export default { | ||||
|       }; | ||||
|       this.assignedAgent = selfAssign; | ||||
|     }, | ||||
|     async onSendReply() { | ||||
|     confirmOnSendReply() { | ||||
|       if (this.isReplyButtonDisabled) { | ||||
|         return; | ||||
|       } | ||||
| @@ -685,6 +710,30 @@ export default { | ||||
|         this.$emit('update:popoutReplyBox', false); | ||||
|       } | ||||
|     }, | ||||
|     async onSendReply() { | ||||
|       const undefinedVariables = getUndefinedVariablesInMessage({ | ||||
|         message: this.message, | ||||
|         variables: this.messageVariables, | ||||
|       }); | ||||
|       if (undefinedVariables.length > 0) { | ||||
|         const undefinedVariablesCount = | ||||
|           undefinedVariables.length > 1 ? undefinedVariables.length : 1; | ||||
|         this.undefinedVariableMessage = this.$t( | ||||
|           'CONVERSATION.REPLYBOX.UNDEFINED_VARIABLES.MESSAGE', | ||||
|           { | ||||
|             undefinedVariablesCount, | ||||
|             undefinedVariables: undefinedVariables.join(', '), | ||||
|           } | ||||
|         ); | ||||
|  | ||||
|         const ok = await this.$refs.confirmDialog.showConfirmation(); | ||||
|         if (ok) { | ||||
|           this.confirmOnSendReply(); | ||||
|         } | ||||
|       } else { | ||||
|         this.confirmOnSendReply(); | ||||
|       } | ||||
|     }, | ||||
|     async sendMessage(messagePayload) { | ||||
|       try { | ||||
|         await this.$store.dispatch( | ||||
| @@ -707,9 +756,13 @@ export default { | ||||
|       this.hideWhatsappTemplatesModal(); | ||||
|     }, | ||||
|     replaceText(message) { | ||||
|       const updatedMessage = replaceVariablesInMessage({ | ||||
|         message, | ||||
|         variables: this.messageVariables, | ||||
|       }); | ||||
|       setTimeout(() => { | ||||
|         this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); | ||||
|         this.message = message; | ||||
|         this.message = updatedMessage; | ||||
|       }, 100); | ||||
|     }, | ||||
|     setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) { | ||||
|   | ||||
| @@ -0,0 +1,37 @@ | ||||
| <template> | ||||
|   <mention-box :items="items" @mention-select="handleVariableClick" /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { MESSAGE_VARIABLES } from 'shared/constants/messages'; | ||||
| import MentionBox from '../mentions/MentionBox.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { MentionBox }, | ||||
|   props: { | ||||
|     searchKey: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     items() { | ||||
|       return MESSAGE_VARIABLES.filter(variable => { | ||||
|         return ( | ||||
|           variable.label.includes(this.searchKey) || | ||||
|           variable.key.includes(this.searchKey) | ||||
|         ); | ||||
|       }).map(variable => ({ | ||||
|         label: variable.key, | ||||
|         key: variable.key, | ||||
|         description: variable.label, | ||||
|       })); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     handleVariableClick(item = {}) { | ||||
|       this.$emit('click', item.key); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -3,6 +3,7 @@ export const CONVERSATION_EVENTS = Object.freeze({ | ||||
|   SENT_MESSAGE: 'Sent a message', | ||||
|   SENT_PRIVATE_NOTE: 'Sent a private note', | ||||
|   INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response', | ||||
|   INSERTED_A_VARIABLE: 'Inserted a variable', | ||||
|   USED_MENTIONS: 'Used mentions', | ||||
|  | ||||
|   APPLY_FILTER: 'Applied filters in the conversation list', | ||||
|   | ||||
							
								
								
									
										59
									
								
								app/javascript/dashboard/helper/messageHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/javascript/dashboard/helper/messageHelper.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| const MESSAGE_VARIABLES_REGEX = /{{(.*?)}}/g; | ||||
| export const replaceVariablesInMessage = ({ message, variables }) => { | ||||
|   return message.replace(MESSAGE_VARIABLES_REGEX, (match, replace) => { | ||||
|     return variables[replace.trim()] | ||||
|       ? variables[replace.trim().toLowerCase()] | ||||
|       : ''; | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const skipCodeBlocks = str => str.replace(/```(?:.|\n)+?```/g, ''); | ||||
|  | ||||
| export const getFirstName = ({ user }) => { | ||||
|   return user?.name ? user.name.split(' ').shift() : ''; | ||||
| }; | ||||
|  | ||||
| export const getLastName = ({ user }) => { | ||||
|   if (user && user.name) { | ||||
|     return user.name.split(' ').length > 1 ? user.name.split(' ').pop() : ''; | ||||
|   } | ||||
|   return ''; | ||||
| }; | ||||
|  | ||||
| export const getMessageVariables = ({ conversation }) => { | ||||
|   const { | ||||
|     meta: { assignee = {}, sender = {} }, | ||||
|     id, | ||||
|   } = conversation; | ||||
|  | ||||
|   return { | ||||
|     'contact.name': sender?.name, | ||||
|     'contact.first_name': getFirstName({ user: sender }), | ||||
|     'contact.last_name': getLastName({ user: sender }), | ||||
|     'contact.email': sender?.email, | ||||
|     'contact.phone': sender?.phone_number, | ||||
|     'contact.id': sender?.id, | ||||
|     'conversation.id': id, | ||||
|     'agent.name': assignee?.name ? assignee?.name : '', | ||||
|     'agent.first_name': getFirstName({ user: assignee }), | ||||
|     'agent.last_name': getLastName({ user: assignee }), | ||||
|     'agent.email': assignee?.email ?? '', | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const getUndefinedVariablesInMessage = ({ message, variables }) => { | ||||
|   const messageWithOutCodeBlocks = skipCodeBlocks(message); | ||||
|   const matches = messageWithOutCodeBlocks.match(MESSAGE_VARIABLES_REGEX); | ||||
|   if (!matches) return []; | ||||
|  | ||||
|   return matches | ||||
|     .map(match => { | ||||
|       return match | ||||
|         .replace('{{', '') | ||||
|         .replace('}}', '') | ||||
|         .trim(); | ||||
|     }) | ||||
|     .filter(variable => { | ||||
|       return !variables[variable]; | ||||
|     }); | ||||
| }; | ||||
							
								
								
									
										138
									
								
								app/javascript/dashboard/helper/specs/messageHelper.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								app/javascript/dashboard/helper/specs/messageHelper.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | ||||
| import { | ||||
|   replaceVariablesInMessage, | ||||
|   getFirstName, | ||||
|   getLastName, | ||||
|   getMessageVariables, | ||||
|   getUndefinedVariablesInMessage, | ||||
| } from '../messageHelper'; | ||||
|  | ||||
| const variables = { | ||||
|   'contact.name': 'John Doe', | ||||
|   'contact.first_name': 'John', | ||||
|   'contact.last_name': 'Doe', | ||||
|   'contact.email': 'john.p@example.com', | ||||
|   'contact.phone': '1234567890', | ||||
|   'conversation.id': 1, | ||||
|   'agent.first_name': 'Samuel', | ||||
|   'agent.last_name': 'Smith', | ||||
|   'agent.email': 'samuel@gmail.com', | ||||
| }; | ||||
|  | ||||
| describe('#replaceVariablesInMessage', () => { | ||||
|   it('returns the message with variable name', () => { | ||||
|     const message = | ||||
|       'No issues. Hey {{contact.first_name}}, we will send the reset instructions to your email {{ contact.email}}. The {{ agent.first_name }} {{ agent.last_name }} will take care of everything. Your conversation id is {{ conversation.id }}.'; | ||||
|     expect(replaceVariablesInMessage({ message, variables })).toBe( | ||||
|       'No issues. Hey John, we will send the reset instructions to your email john.p@example.com. The Samuel Smith will take care of everything. Your conversation id is 1.' | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   it('returns the message with variable name having white space', () => { | ||||
|     const message = 'hey {{contact.name}} how may I help you?'; | ||||
|     expect(replaceVariablesInMessage({ message, variables })).toBe( | ||||
|       'hey John Doe how may I help you?' | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   it('returns the message with variable email', () => { | ||||
|     const message = | ||||
|       'No issues. We will send the reset instructions to your email at {{contact.email}}'; | ||||
|     expect(replaceVariablesInMessage({ message, variables })).toBe( | ||||
|       'No issues. We will send the reset instructions to your email at john.p@example.com' | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   it('returns the message with multiple variables', () => { | ||||
|     const message = | ||||
|       'hey {{ contact.name }}, no issues. We will send the reset instructions to your email at {{contact.email}}'; | ||||
|     expect(replaceVariablesInMessage({ message, variables })).toBe( | ||||
|       'hey John Doe, no issues. We will send the reset instructions to your email at john.p@example.com' | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   it('returns the message if the variable is not present in variables', () => { | ||||
|     const message = 'Please dm me at {{contact.twitter}}'; | ||||
|     expect(replaceVariablesInMessage({ message, variables })).toBe( | ||||
|       'Please dm me at ' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| describe('#getFirstName', () => { | ||||
|   it('returns the first name of the contact', () => { | ||||
|     const assignee = { name: 'John Doe' }; | ||||
|     expect(getFirstName({ user: assignee })).toBe('John'); | ||||
|   }); | ||||
|  | ||||
|   it('returns the first name of the contact with multiple names', () => { | ||||
|     const assignee = { name: 'John Doe Smith' }; | ||||
|     expect(getFirstName({ user: assignee })).toBe('John'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| describe('#getLastName', () => { | ||||
|   it('returns the last name of the contact', () => { | ||||
|     const assignee = { name: 'John Doe' }; | ||||
|     expect(getLastName({ user: assignee })).toBe('Doe'); | ||||
|   }); | ||||
|  | ||||
|   it('returns the last name of the contact with multiple names', () => { | ||||
|     const assignee = { name: 'John Doe Smith' }; | ||||
|     expect(getLastName({ user: assignee })).toBe('Smith'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| describe('#getMessageVariables', () => { | ||||
|   it('returns the variables', () => { | ||||
|     const conversation = { | ||||
|       meta: { | ||||
|         assignee: { | ||||
|           name: 'Samuel Smith', | ||||
|           email: 'samuel@example.com', | ||||
|         }, | ||||
|         sender: { | ||||
|           name: 'John Doe', | ||||
|           email: 'john.doe@gmail.com', | ||||
|           phone_number: '1234567890', | ||||
|         }, | ||||
|       }, | ||||
|       id: 1, | ||||
|     }; | ||||
|     expect(getMessageVariables({ conversation })).toEqual({ | ||||
|       'contact.name': 'John Doe', | ||||
|       'contact.first_name': 'John', | ||||
|       'contact.last_name': 'Doe', | ||||
|       'contact.email': 'john.doe@gmail.com', | ||||
|       'contact.phone': '1234567890', | ||||
|       'conversation.id': 1, | ||||
|       'agent.name': 'Samuel Smith', | ||||
|       'agent.first_name': 'Samuel', | ||||
|       'agent.last_name': 'Smith', | ||||
|       'agent.email': 'samuel@example.com', | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| describe('#getUndefinedVariablesInMessage', () => { | ||||
|   it('returns the undefined variables', () => { | ||||
|     const message = 'Please dm me at {{contact.twitter}}'; | ||||
|     expect( | ||||
|       getUndefinedVariablesInMessage({ message, variables }).length | ||||
|     ).toEqual(1); | ||||
|     expect(getUndefinedVariablesInMessage({ message, variables })).toEqual( | ||||
|       expect.arrayContaining(['contact.twitter']) | ||||
|     ); | ||||
|   }); | ||||
|   it('skip variables in string with code blocks', () => { | ||||
|     const message = | ||||
|       'hey {{contact_name}} how are you? ``` code: {{contact_name}} ```'; | ||||
|     const undefinedVariables = getUndefinedVariablesInMessage({ | ||||
|       message, | ||||
|       variables, | ||||
|     }); | ||||
|     expect(undefinedVariables.length).toEqual(1); | ||||
|     expect(undefinedVariables).toEqual( | ||||
|       expect.arrayContaining(['contact_name']) | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
| @@ -132,6 +132,14 @@ | ||||
|           "PLACEHOLDER": "Emails separated by commas", | ||||
|           "ERROR": "Please enter valid email addresses" | ||||
|         } | ||||
|       }, | ||||
|       "UNDEFINED_VARIABLES": { | ||||
|         "TITLE": "Undefined variables", | ||||
|         "MESSAGE": "You have {undefinedVariablesCount} undefined variables in your message: {undefinedVariables}. Would you like to send the message anyway?", | ||||
|         "CONFIRM": { | ||||
|           "YES": "Send", | ||||
|           "CANCEL": "Cancel" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", | ||||
|   | ||||
| @@ -79,6 +79,7 @@ | ||||
|                   v-model="message" | ||||
|                   class="message-editor" | ||||
|                   :class="{ editor_warning: $v.message.$error }" | ||||
|                   :enable-variables="true" | ||||
|                   :placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')" | ||||
|                   @toggle-canned-menu="toggleCannedMenu" | ||||
|                   @blur="$v.message.$touch" | ||||
|   | ||||
| @@ -21,14 +21,18 @@ | ||||
|         <div class="medium-12 columns"> | ||||
|           <label :class="{ error: $v.content.$error }"> | ||||
|             {{ $t('CANNED_MGMT.ADD.FORM.CONTENT.LABEL') }} | ||||
|             <textarea | ||||
|               v-model.trim="content" | ||||
|               rows="5" | ||||
|               type="text" | ||||
|             <label class="editor-wrap"> | ||||
|               <woot-message-editor | ||||
|                 v-model="content" | ||||
|                 class="message-editor" | ||||
|                 :class="{ editor_warning: $v.content.$error }" | ||||
|                 :enable-variables="true" | ||||
|                 :enable-canned-responses="false" | ||||
|                 :placeholder="$t('CANNED_MGMT.ADD.FORM.CONTENT.PLACEHOLDER')" | ||||
|               @input="$v.content.$touch" | ||||
|                 @blur="$v.content.$touch" | ||||
|               /> | ||||
|             </label> | ||||
|           </label> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|           <div class="medium-12 columns"> | ||||
| @@ -56,12 +60,14 @@ import { required, minLength } from 'vuelidate/lib/validators'; | ||||
|  | ||||
| import WootSubmitButton from '../../../../components/buttons/FormSubmitButton'; | ||||
| import Modal from '../../../../components/Modal'; | ||||
| import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor'; | ||||
| import alertMixin from 'shared/mixins/alertMixin'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     WootSubmitButton, | ||||
|     Modal, | ||||
|     WootMessageEditor, | ||||
|   }, | ||||
|   mixins: [alertMixin], | ||||
|   props: { | ||||
| @@ -125,3 +131,11 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| ::v-deep { | ||||
|   .ProseMirror-menubar { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -18,14 +18,18 @@ | ||||
|         <div class="medium-12 columns"> | ||||
|           <label :class="{ error: $v.content.$error }"> | ||||
|             {{ $t('CANNED_MGMT.EDIT.FORM.CONTENT.LABEL') }} | ||||
|             <textarea | ||||
|               v-model.trim="content" | ||||
|               rows="5" | ||||
|               type="text" | ||||
|             <label class="editor-wrap"> | ||||
|               <woot-message-editor | ||||
|                 v-model="content" | ||||
|                 class="message-editor" | ||||
|                 :class="{ editor_warning: $v.content.$error }" | ||||
|                 :enable-variables="true" | ||||
|                 :enable-canned-responses="false" | ||||
|                 :placeholder="$t('CANNED_MGMT.EDIT.FORM.CONTENT.PLACEHOLDER')" | ||||
|               @input="$v.content.$touch" | ||||
|                 @blur="$v.content.$touch" | ||||
|               /> | ||||
|             </label> | ||||
|           </label> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|           <div class="medium-12 columns"> | ||||
| @@ -51,7 +55,7 @@ | ||||
| <script> | ||||
| /* eslint no-console: 0 */ | ||||
| import { required, minLength } from 'vuelidate/lib/validators'; | ||||
|  | ||||
| import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor'; | ||||
| import WootSubmitButton from '../../../../components/buttons/FormSubmitButton'; | ||||
| import Modal from '../../../../components/Modal'; | ||||
|  | ||||
| @@ -59,6 +63,7 @@ export default { | ||||
|   components: { | ||||
|     WootSubmitButton, | ||||
|     Modal, | ||||
|     WootMessageEditor, | ||||
|   }, | ||||
|   props: { | ||||
|     id: { type: Number, default: null }, | ||||
| @@ -139,3 +144,10 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style scoped lang="scss"> | ||||
| ::v-deep { | ||||
|   .ProseMirror-menubar { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -77,3 +77,50 @@ export const AUDIO_FORMATS = { | ||||
|   WEBM: 'audio/webm', | ||||
|   OGG: 'audio/ogg', | ||||
| }; | ||||
|  | ||||
| export const MESSAGE_VARIABLES = [ | ||||
|   { | ||||
|     label: 'Conversation Id', | ||||
|     key: 'conversation.id', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Contact Id', | ||||
|     key: 'contact.id', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Contact name', | ||||
|     key: 'contact.name', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Contact first name', | ||||
|     key: 'contact.first_name', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Contact last name', | ||||
|     key: 'contact.last_name', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Contact email', | ||||
|     key: 'contact.email', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Contact phone', | ||||
|     key: 'contact.phone', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Agent name', | ||||
|     key: 'agent.name', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Agent first name', | ||||
|     key: 'agent.first_name', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Agent last name', | ||||
|     key: 'agent.last_name', | ||||
|   }, | ||||
|   { | ||||
|     label: 'Agent email', | ||||
|     key: 'agent.email', | ||||
|   }, | ||||
| ]; | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@braid/vue-formulate": "^2.5.2", | ||||
|     "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#beta", | ||||
|     "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#variable-mention", | ||||
|     "@chatwoot/utils": "^0.0.10", | ||||
|     "@hcaptcha/vue-hcaptcha": "^0.3.2", | ||||
|     "@june-so/analytics-next": "^1.36.5", | ||||
|   | ||||
| @@ -1391,9 +1391,9 @@ | ||||
|     is-url "^1.2.4" | ||||
|     nanoid "^2.1.11" | ||||
|  | ||||
| "@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git#beta": | ||||
| "@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git#variable-mention": | ||||
|   version "1.0.0" | ||||
|   resolved "https://github.com/chatwoot/prosemirror-schema.git#e74e54cca4acaa4d87f3e0e48d47ffaea283ec88" | ||||
|   resolved "https://github.com/chatwoot/prosemirror-schema.git#2205f322e54517c415d54b013742838f2e5faf89" | ||||
|   dependencies: | ||||
|     prosemirror-commands "^1.1.4" | ||||
|     prosemirror-dropcursor "^1.3.2" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth