mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 11:37:58 +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"
|
@click="insertMentionNode"
|
||||||
/>
|
/>
|
||||||
<canned-response
|
<canned-response
|
||||||
v-if="showCannedMenu && !isPrivate"
|
v-if="shouldShowCannedResponses"
|
||||||
:search-key="cannedSearchTerm"
|
:search-key="cannedSearchTerm"
|
||||||
@click="insertCannedResponse"
|
@click="insertCannedResponse"
|
||||||
/>
|
/>
|
||||||
|
<variable-list
|
||||||
|
v-if="shouldShowVariables"
|
||||||
|
:search-key="variableSearchTerm"
|
||||||
|
@click="insertVariable"
|
||||||
|
/>
|
||||||
<div ref="editor" />
|
<div ref="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,6 +36,7 @@ import {
|
|||||||
|
|
||||||
import TagAgents from '../conversation/TagAgents';
|
import TagAgents from '../conversation/TagAgents';
|
||||||
import CannedResponse from '../conversation/CannedResponse';
|
import CannedResponse from '../conversation/CannedResponse';
|
||||||
|
import VariableList from '../conversation/VariableList';
|
||||||
|
|
||||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
|
|
||||||
@@ -43,6 +49,7 @@ import {
|
|||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
|
import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper';
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
const createState = (content, placeholder, plugins = []) => {
|
const createState = (content, placeholder, plugins = []) => {
|
||||||
@@ -58,7 +65,7 @@ const createState = (content, placeholder, plugins = []) => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WootMessageEditor',
|
name: 'WootMessageEditor',
|
||||||
components: { TagAgents, CannedResponse },
|
components: { TagAgents, CannedResponse, VariableList },
|
||||||
mixins: [eventListenerMixins, uiSettingsMixin],
|
mixins: [eventListenerMixins, uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
@@ -68,13 +75,18 @@ export default {
|
|||||||
enableSuggestions: { type: Boolean, default: true },
|
enableSuggestions: { type: Boolean, default: true },
|
||||||
overrideLineBreaks: { type: Boolean, default: false },
|
overrideLineBreaks: { type: Boolean, default: false },
|
||||||
updateSelectionWith: { type: String, default: '' },
|
updateSelectionWith: { type: String, default: '' },
|
||||||
|
enableVariables: { type: Boolean, default: false },
|
||||||
|
enableCannedResponses: { type: Boolean, default: true },
|
||||||
|
variables: { type: Object, default: () => ({}) },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showUserMentions: false,
|
showUserMentions: false,
|
||||||
showCannedMenu: false,
|
showCannedMenu: false,
|
||||||
|
showVariables: false,
|
||||||
mentionSearchKey: '',
|
mentionSearchKey: '',
|
||||||
cannedSearchTerm: '',
|
cannedSearchTerm: '',
|
||||||
|
variableSearchTerm: '',
|
||||||
editorView: null,
|
editorView: null,
|
||||||
range: null,
|
range: null,
|
||||||
state: undefined,
|
state: undefined,
|
||||||
@@ -84,6 +96,14 @@ export default {
|
|||||||
contentFromEditor() {
|
contentFromEditor() {
|
||||||
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
|
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
|
||||||
},
|
},
|
||||||
|
shouldShowVariables() {
|
||||||
|
return this.enableVariables && this.showVariables && !this.isPrivate;
|
||||||
|
},
|
||||||
|
shouldShowCannedResponses() {
|
||||||
|
return (
|
||||||
|
this.enableCannedResponses && this.showCannedMenu && !this.isPrivate
|
||||||
|
);
|
||||||
|
},
|
||||||
plugins() {
|
plugins() {
|
||||||
if (!this.enableSuggestions) {
|
if (!this.enableSuggestions) {
|
||||||
return [];
|
return [];
|
||||||
@@ -103,6 +123,7 @@ export default {
|
|||||||
this.range = args.range;
|
this.range = args.range;
|
||||||
|
|
||||||
this.mentionSearchKey = args.text.replace('@', '');
|
this.mentionSearchKey = args.text.replace('@', '');
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
@@ -142,6 +163,34 @@ export default {
|
|||||||
return event.keyCode === 13 && this.showCannedMenu;
|
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) {
|
showCannedMenu(updatedValue) {
|
||||||
this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
|
this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
|
||||||
},
|
},
|
||||||
|
showVariables(updatedValue) {
|
||||||
|
this.$emit('toggle-variables-menu', !this.isPrivate && updatedValue);
|
||||||
|
},
|
||||||
value(newValue = '') {
|
value(newValue = '') {
|
||||||
if (newValue !== this.contentFromEditor) {
|
if (newValue !== this.contentFromEditor) {
|
||||||
this.reloadState();
|
this.reloadState();
|
||||||
@@ -267,19 +319,22 @@ export default {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
insertCannedResponse(cannedItem) {
|
insertCannedResponse(cannedItem) {
|
||||||
|
const updatedMessage = replaceVariablesInMessage({
|
||||||
|
message: cannedItem,
|
||||||
|
variables: this.variables,
|
||||||
|
});
|
||||||
if (!this.editorView) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let from = this.range.from - 1;
|
let from = this.range.from - 1;
|
||||||
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
||||||
cannedItem
|
updatedMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
if (node.textContent === cannedItem) {
|
if (node.textContent === updatedMessage) {
|
||||||
node = this.editorView.state.schema.text(cannedItem);
|
node = this.editorView.state.schema.text(updatedMessage);
|
||||||
from = this.range.from;
|
from = this.range.from;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +351,29 @@ export default {
|
|||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
return false;
|
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() {
|
emitOnChange() {
|
||||||
this.editorView.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
|
|||||||
@@ -63,12 +63,15 @@
|
|||||||
:placeholder="messagePlaceHolder"
|
:placeholder="messagePlaceHolder"
|
||||||
:update-selection-with="updateEditorSelectionWith"
|
:update-selection-with="updateEditorSelectionWith"
|
||||||
:min-height="4"
|
:min-height="4"
|
||||||
|
:enable-variables="true"
|
||||||
|
:variables="messageVariables"
|
||||||
@typing-off="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typing-on="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@toggle-user-mention="toggleUserMention"
|
@toggle-user-mention="toggleUserMention"
|
||||||
@toggle-canned-menu="toggleCannedMenu"
|
@toggle-canned-menu="toggleCannedMenu"
|
||||||
|
@toggle-variables-menu="toggleVariablesMenu"
|
||||||
@clear-selection="clearEditorSelection"
|
@clear-selection="clearEditorSelection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,6 +129,12 @@
|
|||||||
@on-send="onSendWhatsAppReply"
|
@on-send="onSendWhatsAppReply"
|
||||||
@cancel="hideWhatsappTemplatesModal"
|
@cancel="hideWhatsappTemplatesModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<woot-confirm-modal
|
||||||
|
ref="confirmDialog"
|
||||||
|
:title="$t('CONVERSATION.REPLYBOX.UNDEFINED_VARIABLES.TITLE')"
|
||||||
|
:description="undefinedVariableMessage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -152,7 +161,11 @@ import {
|
|||||||
AUDIO_FORMATS,
|
AUDIO_FORMATS,
|
||||||
} from 'shared/constants/messages';
|
} from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
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 WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||||
@@ -208,7 +221,6 @@ export default {
|
|||||||
message: '',
|
message: '',
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
showEmojiPicker: false,
|
showEmojiPicker: false,
|
||||||
showMentions: false,
|
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
isRecordingAudio: false,
|
isRecordingAudio: false,
|
||||||
recordingAudioState: '',
|
recordingAudioState: '',
|
||||||
@@ -216,13 +228,16 @@ export default {
|
|||||||
isUploading: false,
|
isUploading: false,
|
||||||
replyType: REPLY_EDITOR_MODES.REPLY,
|
replyType: REPLY_EDITOR_MODES.REPLY,
|
||||||
mentionSearchKey: '',
|
mentionSearchKey: '',
|
||||||
hasUserMention: false,
|
|
||||||
hasSlashCommand: false,
|
hasSlashCommand: false,
|
||||||
bccEmails: '',
|
bccEmails: '',
|
||||||
ccEmails: '',
|
ccEmails: '',
|
||||||
doAutoSaveDraft: () => {},
|
doAutoSaveDraft: () => {},
|
||||||
showWhatsAppTemplatesModal: false,
|
showWhatsAppTemplatesModal: false,
|
||||||
updateEditorSelectionWith: '',
|
updateEditorSelectionWith: '',
|
||||||
|
undefinedVariableMessage: '',
|
||||||
|
showMentions: false,
|
||||||
|
showCannedMenu: false,
|
||||||
|
showVariablesMenu: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -469,6 +484,12 @@ export default {
|
|||||||
}
|
}
|
||||||
return AUDIO_FORMATS.OGG;
|
return AUDIO_FORMATS.OGG;
|
||||||
},
|
},
|
||||||
|
messageVariables() {
|
||||||
|
const variables = getMessageVariables({
|
||||||
|
conversation: this.currentChat,
|
||||||
|
});
|
||||||
|
return variables;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
@@ -610,8 +631,9 @@ export default {
|
|||||||
},
|
},
|
||||||
isAValidEvent(selectedKey) {
|
isAValidEvent(selectedKey) {
|
||||||
return (
|
return (
|
||||||
!this.hasUserMention &&
|
!this.showMentions &&
|
||||||
!this.showCannedMenu &&
|
!this.showCannedMenu &&
|
||||||
|
!this.showVariablesMenu &&
|
||||||
this.isFocused &&
|
this.isFocused &&
|
||||||
isEditorHotKeyEnabled(this.uiSettings, selectedKey)
|
isEditorHotKeyEnabled(this.uiSettings, selectedKey)
|
||||||
);
|
);
|
||||||
@@ -630,11 +652,14 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleUserMention(currentMentionState) {
|
toggleUserMention(currentMentionState) {
|
||||||
this.hasUserMention = currentMentionState;
|
this.showMentions = currentMentionState;
|
||||||
},
|
},
|
||||||
toggleCannedMenu(value) {
|
toggleCannedMenu(value) {
|
||||||
this.showCannedMenu = value;
|
this.showCannedMenu = value;
|
||||||
},
|
},
|
||||||
|
toggleVariablesMenu(value) {
|
||||||
|
this.showVariablesMenu = value;
|
||||||
|
},
|
||||||
openWhatsappTemplateModal() {
|
openWhatsappTemplateModal() {
|
||||||
this.showWhatsAppTemplatesModal = true;
|
this.showWhatsAppTemplatesModal = true;
|
||||||
},
|
},
|
||||||
@@ -664,7 +689,7 @@ export default {
|
|||||||
};
|
};
|
||||||
this.assignedAgent = selfAssign;
|
this.assignedAgent = selfAssign;
|
||||||
},
|
},
|
||||||
async onSendReply() {
|
confirmOnSendReply() {
|
||||||
if (this.isReplyButtonDisabled) {
|
if (this.isReplyButtonDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -685,6 +710,30 @@ export default {
|
|||||||
this.$emit('update:popoutReplyBox', false);
|
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) {
|
async sendMessage(messagePayload) {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(
|
await this.$store.dispatch(
|
||||||
@@ -707,9 +756,13 @@ export default {
|
|||||||
this.hideWhatsappTemplatesModal();
|
this.hideWhatsappTemplatesModal();
|
||||||
},
|
},
|
||||||
replaceText(message) {
|
replaceText(message) {
|
||||||
|
const updatedMessage = replaceVariablesInMessage({
|
||||||
|
message,
|
||||||
|
variables: this.messageVariables,
|
||||||
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
this.message = message;
|
this.message = updatedMessage;
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
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_MESSAGE: 'Sent a message',
|
||||||
SENT_PRIVATE_NOTE: 'Sent a private note',
|
SENT_PRIVATE_NOTE: 'Sent a private note',
|
||||||
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
|
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
|
||||||
|
INSERTED_A_VARIABLE: 'Inserted a variable',
|
||||||
USED_MENTIONS: 'Used mentions',
|
USED_MENTIONS: 'Used mentions',
|
||||||
|
|
||||||
APPLY_FILTER: 'Applied filters in the conversation list',
|
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",
|
"PLACEHOLDER": "Emails separated by commas",
|
||||||
"ERROR": "Please enter valid email addresses"
|
"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",
|
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
v-model="message"
|
v-model="message"
|
||||||
class="message-editor"
|
class="message-editor"
|
||||||
:class="{ editor_warning: $v.message.$error }"
|
:class="{ editor_warning: $v.message.$error }"
|
||||||
|
:enable-variables="true"
|
||||||
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
||||||
@toggle-canned-menu="toggleCannedMenu"
|
@toggle-canned-menu="toggleCannedMenu"
|
||||||
@blur="$v.message.$touch"
|
@blur="$v.message.$touch"
|
||||||
|
|||||||
@@ -21,13 +21,17 @@
|
|||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
<label :class="{ error: $v.content.$error }">
|
<label :class="{ error: $v.content.$error }">
|
||||||
{{ $t('CANNED_MGMT.ADD.FORM.CONTENT.LABEL') }}
|
{{ $t('CANNED_MGMT.ADD.FORM.CONTENT.LABEL') }}
|
||||||
<textarea
|
<label class="editor-wrap">
|
||||||
v-model.trim="content"
|
<woot-message-editor
|
||||||
rows="5"
|
v-model="content"
|
||||||
type="text"
|
class="message-editor"
|
||||||
:placeholder="$t('CANNED_MGMT.ADD.FORM.CONTENT.PLACEHOLDER')"
|
:class="{ editor_warning: $v.content.$error }"
|
||||||
@input="$v.content.$touch"
|
:enable-variables="true"
|
||||||
/>
|
:enable-canned-responses="false"
|
||||||
|
:placeholder="$t('CANNED_MGMT.ADD.FORM.CONTENT.PLACEHOLDER')"
|
||||||
|
@blur="$v.content.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -56,12 +60,14 @@ import { required, minLength } from 'vuelidate/lib/validators';
|
|||||||
|
|
||||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
|
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
|
||||||
import Modal from '../../../../components/Modal';
|
import Modal from '../../../../components/Modal';
|
||||||
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootSubmitButton,
|
WootSubmitButton,
|
||||||
Modal,
|
Modal,
|
||||||
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
mixins: [alertMixin],
|
||||||
props: {
|
props: {
|
||||||
@@ -125,3 +131,11 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
::v-deep {
|
||||||
|
.ProseMirror-menubar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -18,13 +18,17 @@
|
|||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
<label :class="{ error: $v.content.$error }">
|
<label :class="{ error: $v.content.$error }">
|
||||||
{{ $t('CANNED_MGMT.EDIT.FORM.CONTENT.LABEL') }}
|
{{ $t('CANNED_MGMT.EDIT.FORM.CONTENT.LABEL') }}
|
||||||
<textarea
|
<label class="editor-wrap">
|
||||||
v-model.trim="content"
|
<woot-message-editor
|
||||||
rows="5"
|
v-model="content"
|
||||||
type="text"
|
class="message-editor"
|
||||||
:placeholder="$t('CANNED_MGMT.EDIT.FORM.CONTENT.PLACEHOLDER')"
|
:class="{ editor_warning: $v.content.$error }"
|
||||||
@input="$v.content.$touch"
|
:enable-variables="true"
|
||||||
/>
|
:enable-canned-responses="false"
|
||||||
|
:placeholder="$t('CANNED_MGMT.EDIT.FORM.CONTENT.PLACEHOLDER')"
|
||||||
|
@blur="$v.content.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -51,7 +55,7 @@
|
|||||||
<script>
|
<script>
|
||||||
/* eslint no-console: 0 */
|
/* eslint no-console: 0 */
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
|
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
|
||||||
import Modal from '../../../../components/Modal';
|
import Modal from '../../../../components/Modal';
|
||||||
|
|
||||||
@@ -59,6 +63,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
WootSubmitButton,
|
WootSubmitButton,
|
||||||
Modal,
|
Modal,
|
||||||
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
id: { type: Number, default: null },
|
id: { type: Number, default: null },
|
||||||
@@ -139,3 +144,10 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
::v-deep {
|
||||||
|
.ProseMirror-menubar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -77,3 +77,50 @@ export const AUDIO_FORMATS = {
|
|||||||
WEBM: 'audio/webm',
|
WEBM: 'audio/webm',
|
||||||
OGG: 'audio/ogg',
|
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": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate": "^2.5.2",
|
"@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",
|
"@chatwoot/utils": "^0.0.10",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@june-so/analytics-next": "^1.36.5",
|
"@june-so/analytics-next": "^1.36.5",
|
||||||
|
|||||||
@@ -1391,9 +1391,9 @@
|
|||||||
is-url "^1.2.4"
|
is-url "^1.2.4"
|
||||||
nanoid "^2.1.11"
|
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"
|
version "1.0.0"
|
||||||
resolved "https://github.com/chatwoot/prosemirror-schema.git#e74e54cca4acaa4d87f3e0e48d47ffaea283ec88"
|
resolved "https://github.com/chatwoot/prosemirror-schema.git#2205f322e54517c415d54b013742838f2e5faf89"
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-commands "^1.1.4"
|
prosemirror-commands "^1.1.4"
|
||||||
prosemirror-dropcursor "^1.3.2"
|
prosemirror-dropcursor "^1.3.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user