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>
|
||||
Reference in New Issue
Block a user