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:
Muhsin Keloth
2023-01-24 13:06:50 +05:30
committed by GitHub
parent cab409f3ef
commit d9a1154977
13 changed files with 479 additions and 31 deletions

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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',

View 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];
});
};

View 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'])
);
});
});

View File

@@ -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",

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',
},
];

View File

@@ -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",

View File

@@ -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"